Code Review found 2 critical bugs
还没有人认领这个 Issue。
评估
调研方向
从 cJSON_Utils.c 中的 compose_patch、create_patches、cJSONUtils_FindPointerFromObjectTo 和 apply_patch 开始。阅读相关的公共 API 路径,并运行 tests/ 和 tests/json-patch-tests/ 下现有的测试。当报告的分配失败路径是安全的,并且拒绝将对象移动到其后代中的输入且不修改文档,同时为两个关键发现提供回归覆盖时,即可视为完成。
由索引模型根据 Issue 内容生成。
描述
A code audit of cJSON v1.7.19 against upstream master surfaced six findings that do not appear to have existing tickets after cross-referencing the issue tracker and open PRs. Two are critical and reachable from the public API on attacker-supplied input; the other four are lower-severity and grouped at the end for context.
A summary of previously-known findings this audit also confirmed (so you know I checked the tracker and am not re-filing) is at the bottom.
Critical #1 — NULL dereference on cJSON_malloc failure in cJSON_Utils.c (three call sites)
Three public-API-reachable code paths allocate via cJSON_malloc and use the result with no NULL check. cJSON_malloc delegates to the user's malloc_fn hook, which is trivially returnable as NULL, so on OOM / memory pressure the process segfaults.
Call sites
1. compose_patch — cJSON_Utils.c:1120
unsigned char *full_path = (unsigned char*)cJSON_malloc(path_length + suffix_length + sizeof(\"/\"));
sprintf((char*)full_path, \"%s/\", (const char*)path); // line 1122: deref of possibly-NULL full_path
encode_string_as_pointer(full_path + path_length + 1, suffix);
Reachable via the public cJSONUtils_AddPatchToArray.
2. create_patches — cJSON_Utils.c:1175 (array branch) and :1247 (object branch)
unsigned char *new_path = (unsigned char*)cJSON_malloc(strlen((const char*)path) + 20 + sizeof(\"/\"));
// no NULL check
// ...
sprintf((char*)new_path, \"%s/%lu\", path, (unsigned long)index); // line 1188: deref
Reachable via cJSONUtils_GeneratePatches / cJSONUtils_GeneratePatchesCaseSensitive.
3. cJSONUtils_FindPointerFromObjectTo — cJSON_Utils.c:224 (array branch) and :242 (object branch)
unsigned char *full_pointer = (unsigned char*)cJSON_malloc(strlen((char*)target_pointer) + 20 + sizeof(\"/\"));
if (child_index > ULONG_MAX) { ... } // checks the index, not full_pointer
sprintf((char*)full_pointer, \"/%lu%s\", ...); // line 234: deref
The :242 branch is worse: the very next line (full_pointer[0] = '/'; at :243) is an immediate deref, and on this failure path target_pointer (already allocated by the recursive call) is also leaked.
Suggested fix
if (full_path == NULL) {
/* free any already-allocated siblings, then bail */
return; /* or return NULL, depending on the function */
}
For FindPointerFromObjectTo, the fix additionally needs to cJSON_free(target_pointer) before returning — this also closes the long-standing leak originally reported in #414 (closed but still present on master at line 224).
Verification
No test in tests/*.c exercises malloc failure via cJSON_InitHooks; these paths are unverified under OOM but read as straightforward NULL-check omissions.
Critical #2 — apply_patch move op missing the RFC 6902 §4.4 descendant check
apply_patch in cJSON_Utils.c:905-940 implements the move op as detach_path(from) followed by an add at path, with no check that path is not a descendant of from. RFC 6902 §4.4 explicitly forbids this:
The "from" location MUST NOT be a proper prefix of the "path" location; i.e., a location cannot be moved into one of its children.
Reproducer
Starting document: {\"a\":{\"b\":{}}}
Patch: [{\"op\":\"move\",\"from\":\"/a\",\"path\":\"/a/b\"}]
Current behaviour:
detach_path(\"/a\")succeeds — removes/afrom the document.- The subsequent lookup of the parent
/a/bfails (/ano longer exists). apply_patchreturns error status 9, but the document has already been mutated —ais gone.
Per RFC the patch should be rejected before any mutation. This is both an RFC conformance bug and a data-loss path for any caller that trusts the error return (the earlier ops in a multi-op patch are not rolled back; this is adjacent to but distinct from the atomicity concern in #997).
Suggested fix
Before the detach_path(from) call at line 918, validate:
if (opcode == MOVE) {
const char *f = from->valuestring;
const char *p = path->valuestring;
size_t f_len = strlen(f);
if (strncmp(p, f, f_len) == 0 && (p[f_len] == '/' || p[f_len] == '\0')) {
status = 14; /* or whichever distinct code */
goto cleanup;
}
}
Verification
No test in tests/json-patch-tests/*.json (spec_tests.json, tests.json, or cjson-utils-tests.json) exercises move-into-descendant — grep for "descendant" or "prefix" in that directory returns no matches.
Other findings not currently in the tracker
Reported briefly; happy to open separate issues if preferred.
Major — generate_merge_patch case-sensitivity inconsistency (cJSON_Utils.c:1423)
Lines 1406-1407 sort both objects with sort_object(..., case_sensitive). The subsequent merge loop at line 1423 compares keys with plain strcmp, ignoring the flag:
diff = strcmp(from_child->string, to_child->string);
When case_sensitive == false and two keys differ only in case (e.g. \"Foo\" vs \"foo\"), sort_object places them adjacent but strcmp reports them as different, producing a spurious add + remove pair in the patch. The sibling code in create_patches at line 1238 gets this right — it uses compare_strings(..., case_sensitive). Fix: use the same helper here.
Minor — ensure() size_t overflow on 32-bit (cJSON.c:512)
if (needed > INT_MAX) { return NULL; }
needed += p->offset + 1; /* can overflow size_t when p->offset is near SIZE_MAX */
Unreachable on 64-bit (you would need > 2 GB already buffered); a theoretical hazard on 32-bit with caller-supplied large buffers. Guard: if (p->offset > SIZE_MAX - 1 - needed) return NULL; before the add.
Minor — cJSON_ReplaceItemInArray silent out-of-range (cJSON.c:2419-2427)
When which exceeds array length, get_array_item returns NULL and cJSON_ReplaceItemViaPointer at line 2370 correctly rejects the call. Not a crash, but the single false return is indistinguishable from NULL-arg / allocation failures. Consider documenting the out-of-range contract, or splitting the status.
Minor — parse_hex4 dual-meaning return (cJSON.c:666-699)
Returns 0 both for the valid sequence 0000 and for any invalid hex digit. Callers (e.g. utf16_literal_to_utf8) disambiguate by pointer position. A comment on the function explaining the dual meaning would help the next reader.
Findings already tracked (confirmed on master, not re-filing)
To close the loop — I also verified these on current master and will not re-file them:
decode_pointer_inplace~1off-by-one — #977, PR #1001, PR #972, PR #998, PR #993 (confirmed with a standalone reproducer:a~1bdecodes toa~/)- Integer overflow in
decode_array_index_from_pointer— PR #1002 cJSON_CompareO(n²) on objects — PR #971cJSON_CreateNumber(NaN)UB via(int)numcast — #999, PR #1000cJSON_MinifyNUL-termination / escape handling — PR #967, PR #1003cJSON_Deleterecursion depth (no guard analogous tocJSON_Duplicate'sCJSON_CIRCULAR_LIMIT) — related to #833 and #995
Happy to expand any of the above into a focused PR if helpful; wanted to get the critical pair in front of maintainers first.
- 主要语言
- C
- 星标
- 13k
- 派生
- 3.5k
- PR 合并指标
- 30 天内没有已合并 PR
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 没有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
DaveGamble/cJSON 的其他 Issue
-
难度 2/5 1-3 小时 新手友好度 86/100
DaveGamble/cJSON#1094 · 1 个 reaction ·
-
难度 2/5 1-3 小时 新手友好度 68/100
DaveGamble/cJSON#1093 ·
-
Use-after-free in cJSONUtils_ApplyPatches when a patch removes the patch array可能已有人在做 @iliasabk 于 24 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 82/100
DaveGamble/cJSON#1082 ·
-
Use-after-free in cJSON_ReplaceItemInObject when the key is the item's own name可能已有人在做 @iliasabk 于 25 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 82/100
DaveGamble/cJSON#1081 ·
-
buffer_skip_whitespace accepts every byte below 0x21 as whitespace可能已有人在做 @AetherAI3 于 36 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 78/100
DaveGamble/cJSON#1074 ·
相似的 Issue
-
bug good first issue
难度 2/5 1-3 小时 新手友好度 82/100
-
难度 2/5 1-3 小时 新手友好度 72/100
-
enhancement
难度 1/5 1 小时以内 新手友好度 85/100
FujiNetWIFI/fujinet-lib#122 · 1 条评论 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 72/100
维护者通常 12 天内回复
-
bug format I/O medium priority rt_lib
难度 2/5 1-3 小时 新手友好度 72/100
维护者通常 1 天内回复