Skip to content

Null pointer dereference in S2J_STRUCT_GET_string_ELEMENT when JSON value is not a string #37

@fa1c4

Description

@fa1c4

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


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:

  1. cJSON_GetObjectItem(from_json, "name") is case-insensitive and returns a cJSON * node whenever the key exists, regardless of the value type (string, number, object, etc.).
  2. For numeric values, cJSON sets type == cJSON_Number and populates valueint / valuedouble, but leaves valuestring == NULL for that node.
  3. S2J_STRUCT_GET_string_ELEMENT only checks whether json_temp is 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 the nAMe key
    (case-insensitive comparison), so json_temp != NULL.
  • The value is numeric, so json_temp->valuestring == NULL.
  • The macro calls strncpy with 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:

  1. Uses struct2json to map JSON input into C structs, and
  2. 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.

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