Repository navigation
fix(sdk/c): find JSON keys, not matching values; decode string escapes #65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,60 +15,148 @@ | |
|
|
||
| /* ---------- JSON Parse Helpers ---------- */ | ||
|
|
||
| static int json_find_string(const char *json, const char *key, | ||
| char *out, size_t out_size) { | ||
| #define JSON_BAD_HEX 0xFFFFFFFFu | ||
|
|
||
| /* The value of four hex digits at p, or JSON_BAD_HEX. */ | ||
| static uint32_t json_hex4(const char *p) { | ||
| uint32_t v = 0; | ||
| int i; | ||
| for (i = 0; i < 4; i++) { | ||
| char c = p[i]; | ||
| v <<= 4; | ||
| if (c >= '0' && c <= '9') | ||
| v |= (uint32_t)(c - '0'); | ||
| else if (c >= 'a' && c <= 'f') | ||
| v |= (uint32_t)(c - 'a' + 10); | ||
| else if (c >= 'A' && c <= 'F') | ||
| v |= (uint32_t)(c - 'A' + 10); | ||
| else | ||
| return JSON_BAD_HEX; | ||
| } | ||
| return v; | ||
| } | ||
|
|
||
| /* Write code point cp as UTF-8 into out (room for 4 bytes); return the length. */ | ||
| static size_t json_utf8(uint32_t cp, char *out) { | ||
| if (cp < 0x80) { | ||
| out[0] = (char)cp; | ||
| return 1; | ||
| } | ||
| if (cp < 0x800) { | ||
| out[0] = (char)(0xC0 | (cp >> 6)); | ||
| out[1] = (char)(0x80 | (cp & 0x3F)); | ||
| return 2; | ||
| } | ||
| if (cp < 0x10000) { | ||
| out[0] = (char)(0xE0 | (cp >> 12)); | ||
| out[1] = (char)(0x80 | ((cp >> 6) & 0x3F)); | ||
| out[2] = (char)(0x80 | (cp & 0x3F)); | ||
| return 3; | ||
| } | ||
| out[0] = (char)(0xF0 | (cp >> 18)); | ||
| out[1] = (char)(0x80 | ((cp >> 12) & 0x3F)); | ||
| out[2] = (char)(0x80 | ((cp >> 6) & 0x3F)); | ||
| out[3] = (char)(0x80 | (cp & 0x3F)); | ||
| return 4; | ||
| } | ||
|
|
||
| /* The start of the value for key, or NULL. Only a "key" followed by a colon | ||
| * is a key: strstr() alone also matched the same text as a string value, so | ||
| * in {"status":"error","error":"disk full"} the lookup for "error" landed on | ||
| * the status value and returned nothing. */ | ||
| static const char *json_find_value(const char *json, const char *key) { | ||
| char pattern[128]; | ||
| const char *start, *end; | ||
| size_t len; | ||
| const char *p = json; | ||
| size_t plen; | ||
|
|
||
| snprintf(pattern, sizeof(pattern), "\"%s\"", key); | ||
| start = strstr(json, pattern); | ||
| if (!start) { | ||
| out[0] = '\0'; | ||
| return -1; | ||
| plen = strlen(pattern); | ||
| while ((p = strstr(p, pattern)) != NULL) { | ||
| const char *v = p + plen; | ||
| while (*v == ' ' || *v == '\t' || *v == '\n' || *v == '\r') | ||
| v++; | ||
| if (*v == ':') { | ||
|
Comment on lines
+74
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · Decoders return embedded field values When an earlier string contains escaped field-like text, or a nested object contains the requested field before the root-level field, Parse the JSON while tracking strings, escapes, and object depth, and return only a matching root-level member. Prompt for AI agentsIn `sdk/c/src/eipc_json.c` at lines 74-78:
**Decoders return embedded field values**
When an earlier string contains escaped field-like text, or a nested object contains the requested field before the root-level field, `json_find_value` scans raw text and accepts the first matching `"key":` without checking whether it is inside a string or at the requested object depth, so decoders return the embedded or nested value instead of the root-level field.
Parse the JSON while tracking strings, escapes, and object depth, and return only a matching root-level member. |
||
| v++; | ||
| while (*v == ' ' || *v == '\t' || *v == '\n' || *v == '\r') | ||
| v++; | ||
| return v; | ||
| } | ||
| p += plen; | ||
| } | ||
| return NULL; | ||
| } | ||
|
|
||
| start += strlen(pattern); | ||
| while (*start == ' ' || *start == ':' || *start == '\t' || *start == '\n') | ||
| start++; | ||
| static int json_find_string(const char *json, const char *key, | ||
| char *out, size_t out_size) { | ||
| const char *start, *end; | ||
| size_t len; | ||
|
|
||
| if (*start != '"') { | ||
| start = json_find_value(json, key); | ||
| if (!start || *start != '"') { | ||
| out[0] = '\0'; | ||
| return -1; | ||
| } | ||
| start++; | ||
|
|
||
| /* Decode the escapes as the value is copied: the encoders write \" \\ | ||
| * \n \r \t (json_escape), and other JSON encoders also write \/ \b \f | ||
| * and \uXXXX (Go's encoding/json writes < > & as \u003c \u003e | ||
| * \u0026). A value longer than out_size - 1 bytes is truncated as | ||
| * before; a decoded \uXXXX is never cut in half. */ | ||
| len = 0; | ||
| end = start; | ||
| while (*end && *end != '"') { | ||
| if (*end == '\\' && *(end + 1)) | ||
| char enc[4]; | ||
| size_t n = 1; | ||
|
|
||
| if (*end != '\\') { | ||
| enc[0] = *end++; | ||
| } else if (end[1] == 'u') { | ||
| uint32_t cp = json_hex4(end + 2); | ||
| end += (cp == JSON_BAD_HEX) ? 2 : 6; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · Malformed escapes alter decoded text When input contains an unknown escape such as Replace each malformed escape with U+FFFD and consume the malformed escape so its remaining characters are not copied as ordinary text. Also at Prompt for AI agentsIn `sdk/c/src/eipc_json.c` at line 116:
**Malformed escapes alter decoded text**
When input contains an unknown escape such as `\q` or an invalid `\u` sequence, `json_find_string` copies the escaped character and drops the backslash for unknown escapes, while invalid `\u` sequences can leave following characters to be copied; callers receive altered text instead of the documented U+FFFD replacement.
Replace each malformed escape with U+FFFD and consume the malformed escape so its remaining characters are not copied as ordinary text.
Also at `sdk/c/src/eipc_json.c:136-137`.From Validate every input at the boundary it enters: type, range, length, format and
allowed values. Reject what does not match rather than coercing it. Validation
performed only in the client is not validation. |
||
| if (cp >= 0xD800 && cp <= 0xDBFF && end[0] == '\\' && end[1] == 'u') { | ||
| uint32_t lo = json_hex4(end + 2); | ||
| if (lo >= 0xDC00 && lo <= 0xDFFF) { | ||
| cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); | ||
| end += 6; | ||
| } | ||
| } | ||
| /* A NUL would cut the C string short, so \u0000 becomes | ||
| * U+FFFD, like a malformed escape or an unpaired surrogate. */ | ||
| if (cp == 0 || cp == JSON_BAD_HEX || (cp >= 0xD800 && cp <= 0xDFFF)) | ||
| cp = 0xFFFD; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · Malformed escape regressions go unnoticed When an input contains malformed Add public-decoder regression tests asserting the specified output for malformed Unicode escapes, unpaired surrogates, and a trailing backslash. Also at Prompt for AI agentsIn `sdk/c/src/eipc_json.c` at line 127:
**Malformed escape regressions go unnoticed**
When an input contains malformed `\u` hex, an unpaired surrogate, or a trailing backslash, the U+FFFD handling and trailing-backslash branches have no regression assertions, so changes that break these decoder paths still pass the C tests and callers receive incorrectly decoded strings.
Add public-decoder regression tests asserting the specified output for malformed Unicode escapes, unpaired surrogates, and a trailing backslash.
Also at `sdk/c/src/eipc_json.c:140`.From - **Unit** — logic and its branches, including edge cases. |
||
| n = json_utf8(cp, enc); | ||
| } else if (end[1]) { | ||
| switch (end[1]) { | ||
| case 'b': enc[0] = '\b'; break; | ||
| case 'f': enc[0] = '\f'; break; | ||
| case 'n': enc[0] = '\n'; break; | ||
| case 'r': enc[0] = '\r'; break; | ||
| case 't': enc[0] = '\t'; break; | ||
| default: enc[0] = end[1]; break; /* \" \\ \/ */ | ||
| } | ||
| end += 2; | ||
| else | ||
| end++; | ||
| } else { | ||
| break; /* a lone backslash at the end of the input */ | ||
| } | ||
|
|
||
| if (len + n >= out_size) | ||
| break; | ||
| memcpy(out + len, enc, n); | ||
| len += n; | ||
| } | ||
|
|
||
| len = (size_t)(end - start); | ||
| if (len >= out_size) | ||
| len = out_size - 1; | ||
| memcpy(out, start, len); | ||
| out[len] = '\0'; | ||
| return 0; | ||
| } | ||
|
|
||
| static int json_find_uint64(const char *json, const char *key, uint64_t *out) { | ||
| char pattern[128]; | ||
| const char *start; | ||
|
|
||
| snprintf(pattern, sizeof(pattern), "\"%s\"", key); | ||
| start = strstr(json, pattern); | ||
| const char *start = json_find_value(json, key); | ||
| if (!start) { | ||
| *out = 0; | ||
| return -1; | ||
| } | ||
|
|
||
| start += strlen(pattern); | ||
| while (*start == ' ' || *start == ':' || *start == '\t' || *start == '\n') | ||
| start++; | ||
|
|
||
| if (*start == '"') | ||
| start++; | ||
| *out = (uint64_t)strtoull(start, NULL, 10); | ||
|
|
@@ -86,20 +174,12 @@ static int json_find_int(const char *json, const char *key, int *out) { | |
| } | ||
|
|
||
| static int json_find_float(const char *json, const char *key, float *out) { | ||
| char pattern[128]; | ||
| const char *start; | ||
|
|
||
| snprintf(pattern, sizeof(pattern), "\"%s\"", key); | ||
| start = strstr(json, pattern); | ||
| const char *start = json_find_value(json, key); | ||
| if (!start) { | ||
| *out = 0.0f; | ||
| return -1; | ||
| } | ||
|
|
||
| start += strlen(pattern); | ||
| while (*start == ' ' || *start == ':' || *start == '\t' || *start == '\n') | ||
| start++; | ||
|
|
||
| if (*start == '"') | ||
| start++; | ||
| *out = (float)strtod(start, NULL); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium · Truncated escapes read past input
When a JSON string ends with a truncated
\uescape,json_hex4reads four bytes without checking that they remain before the input terminator, causing an out-of-bounds read and undefined behavior.Check that four hex digits remain before reading them, and handle truncated escapes without reading past the input.
Prompt for AI agents