Skip to content

[cJSON_Utils] decode_pointer_inplace off-by-one causes incorrect decoding of "~1" (JSON Pointer) #977

@zchengchen

Description

@zchengchen

Description

decode_pointer_inplace() (used by JSON Patch helpers) writes decoded slashes to decoded_string[1] instead of the current output slot. As a result, pointers containing ~1 are decoded incorrectly during patch application—values are written under mutated object keys like "a~/" instead of the intended "a/b". The function reports success even though it corrupted the object.

Reproduction steps

  1. Clone cJSON (current master).
  2. Drop the PoC below into poc_decode_pointer.c at repo root.
  3. Build and run:
    cd cJSON
    gcc -std=c99 -Wall -Wextra poc_decode_pointer.c cJSON.c cJSON_Utils.c -o poc_decode_pointer
    ./poc_decode_pointer

PoC source

#include <stdio.h>
#include <stdlib.h>

#include "cJSON.h"
#include "cJSON_Utils.h"

static void print_json(const char *label, cJSON *item)
{
    char *rendered = cJSON_PrintUnformatted(item);
    if (rendered == NULL)
    {
        fprintf(stderr, "%s: <print error>\n", label);
        return;
    }

    printf("%s: %s\n", label, rendered);
    cJSON_free(rendered);
}

int main(void)
{
    const char *document_text = "{\"a/b\":1}";
    const char *patch_text = "[{\"op\":\"add\",\"path\":\"/a~1b\",\"value\":2}]";
    cJSON *document = NULL;
    cJSON *patches = NULL;
    cJSON *value = NULL;
    int status = 0;

    document = cJSON_Parse(document_text);
    patches = cJSON_Parse(patch_text);
    if ((document == NULL) || (patches == NULL))
    {
        fprintf(stderr, "failed to parse JSON input\n");
        goto cleanup;
    }

    print_json("Original document", document);
    print_json("Patch", patches);

    status = cJSONUtils_ApplyPatches(document, patches);
    printf("cJSONUtils_ApplyPatches returned %d\n", status);

    print_json("Document after patch", document);

    value = cJSON_GetObjectItemCaseSensitive(document, "a/b");
    if (value != NULL)
    {
        printf("a/b = %d\n", value->valueint);
    }
    else
    {
        printf("a/b key is missing!\n");
    }

    value = cJSON_GetObjectItemCaseSensitive(document, "a~/");
    if (value != NULL)
    {
        printf("Corrupted key a~/ = %d\n", value->valueint);
    }

cleanup:
    if (document != NULL)
    {
        cJSON_Delete(document);
    }
    if (patches != NULL)
    {
        cJSON_Delete(patches);
    }

    return status;
}

PoC Output

zc@docker:~/cJSON$ ./poc_decode_pointer 
Original document: {"a/b":1}
Patch: [{"op":"add","path":"/a~1b","value":2}]
cJSONUtils_ApplyPatches returned 0
Document after patch: {"a/b":1,"a~/":2}
a/b = 1
Corrupted key a~/ = 2

Expected vs. actual results

  • Expected: Applying the patch that targets /a~1b updates key "a/b" to 2.
  • Actual: cJSONUtils_ApplyPatches returns success but leaves "a/b" unchanged and inserts a new key "a~/" with value 2, proving the decoded pointer got corrupted.

Suggested fix

In decode_pointer_inplace() (around line 370), write the decoded slash to decoded_string[0] and ensure the output pointer only advances once per decoded character:

diff --git a/cJSON_Utils.c b/cJSON_Utils.c
index 8fa24f8..8a3d881 100644
--- a/cJSON_Utils.c
+++ b/cJSON_Utils.c
@@ -374,7 +374,7 @@ static void decode_pointer_inplace(unsigned char
*string)
             }
             else if (string[1] == '1')
             {
-                decoded_string[1] = '/';
+                decoded_string[0] = '/';
             }
             else
             {
@@ -383,7 +383,10 @@ static void decode_pointer_inplace(unsigned char
*string)
             }

             string++;
+            continue;
         }
+
+        decoded_string[0] = string[0];
     }

     decoded_string[0] = '\0';

This change keeps the decoded pointer aligned with the output buffer so /a~1b resolves to "a/b" during JSON Patch operations.

Output after fix

zc@docker:~/cJSON$ ./poc_decode_pointer 
Original document: {"a/b":1}
Patch: [{"op":"add","path":"/a~1b","value":2}]
cJSONUtils_ApplyPatches returned 0
Document after patch: {"a/b":2}
a/b = 2

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