From 89d49a90a809a33facf86eb28b97d7c4c8450fb7 Mon Sep 17 00:00:00 2001 From: Robert Stepanek Date: Wed, 26 Jul 2023 14:58:10 +0200 Subject: [PATCH] jmap_util: guard array patch with PATCH_ALLOW_ARRAY And reject invalid array indexes. Signed-off-by: Robert Stepanek --- cunit/jmap_util.testc | 113 +++++++++++++++++++++++++++++++++++++++++- imap/jmap_util.c | 65 +++++++++++++++++------- imap/jmap_util.h | 9 ++-- 3 files changed, 163 insertions(+), 24 deletions(-) diff --git a/cunit/jmap_util.testc b/cunit/jmap_util.testc index 7e89fc2a480..c2ed15072d9 100644 --- a/cunit/jmap_util.testc +++ b/cunit/jmap_util.testc @@ -18,7 +18,7 @@ static void test_patchobject(void) char *swant = json_dumps(jwant, JSON_SORT_KEYS|JSON_ENCODE_ANY); \ char *sdiff = json_dumps(jdiff, JSON_SORT_KEYS|JSON_ENCODE_ANY); \ CU_ASSERT_STRING_EQUAL(swant, sdiff); \ - json_t *jback = jmap_patchobject_apply(jfrom, jdiff, NULL); \ + json_t *jback = jmap_patchobject_apply(jfrom, jdiff, NULL, myflags); \ char *sback = json_dumps(jback, JSON_SORT_KEYS|JSON_ENCODE_ANY); \ char *sdest = json_dumps(jdest, JSON_SORT_KEYS|JSON_ENCODE_ANY); \ CU_ASSERT_STRING_EQUAL(sdest, sback); \ @@ -182,6 +182,117 @@ static void test_patchobject(void) #undef TESTCASE } +static void test_patchobject_invalid(void) +{ +#define TESTCASE(_from, _patch, _want_invalid, _flags) \ + { \ + unsigned myflags = (_flags); \ + json_t *jfrom = json_loads((_from), JSON_DECODE_ANY, NULL); \ + json_t *jpatch = json_loads((_patch), JSON_DECODE_ANY, NULL); \ + json_t *jwant_invalid = json_loads((_want_invalid), JSON_DECODE_ANY, NULL); \ + json_t *jhave_invalid = json_array(); \ + json_t *jhave = jmap_patchobject_apply(jfrom, jpatch, jhave_invalid, myflags); \ + CU_ASSERT_PTR_NULL(jhave); \ + char *want_invalid = json_dumps(jwant_invalid, JSON_SORT_KEYS|JSON_ENCODE_ANY); \ + char *have_invalid = json_dumps(jhave_invalid, JSON_SORT_KEYS|JSON_ENCODE_ANY); \ + CU_ASSERT_STRING_EQUAL(want_invalid, have_invalid); \ + free(have_invalid); \ + free(want_invalid); \ + json_decref(jhave_invalid); \ + json_decref(jwant_invalid); \ + json_decref(jpatch); \ + json_decref(jfrom); \ + } + + const char *from, *patch, *want_invalid; + + /* Set non-existent member */ + from = "{" + " \"a\": \"foo\"" + "}"; + patch = "{" + " \"x/y\": \"bar\"" + "}"; + want_invalid = "[" + "\"x/y\"" + "]"; + TESTCASE(from, patch, want_invalid, 0); + + /* Remove non-existent member */ + from = "{" + " \"a\": \"foo\"" + "}"; + patch = "{" + " \"x/y\": null" + "}"; + want_invalid = "[" + "\"x/y\"" + "]"; + TESTCASE(from, patch, want_invalid, 0); + + /* Patch inside array - but no PATCH_ALLOW_ARRAY flag */ + from = "{" + " \"a\": [" + " \"foo\"," + " \"bar\"" + " ]" + "}"; + patch = "{" + " \"a/1\": \"bam\"" + "}"; + want_invalid = "[" + " \"a/1\" " + "]"; + TESTCASE(from, patch, want_invalid, 0); + + /* Delete from array */ + from = "{" + " \"a\": [" + " \"foo\"," + " \"bar\"" + " ]" + "}"; + patch = "{" + " \"a/1\": null" + "}"; + want_invalid = "[" + " \"a/1\" " + "]"; + TESTCASE(from, patch, want_invalid, PATCH_ALLOW_ARRAY); + + /* Patch non-existent array entry */ + from = "{" + " \"a\": [" + " \"foo\"," + " \"bar\"" + " ]" + "}"; + patch = "{" + " \"a/2\": \"bam\"" + "}"; + want_invalid = "[" + " \"a/2\" " + "]"; + TESTCASE(from, patch, want_invalid, PATCH_ALLOW_ARRAY); + + /* Patch with special JSON pointer '-' */ + from = "{" + " \"a\": [" + " \"foo\"," + " \"bar\"" + " ]" + "}"; + patch = "{" + " \"a/-\": \"bam\"" + "}"; + want_invalid = "[" + " \"a/-\" " + "]"; + TESTCASE(from, patch, want_invalid, PATCH_ALLOW_ARRAY); + +#undef TESTCASE +} + static void test_decode_to_utf8(void) { struct testcase { diff --git a/imap/jmap_util.c b/imap/jmap_util.c index 57119249dcc..4e07fecec70 100644 --- a/imap/jmap_util.c +++ b/imap/jmap_util.c @@ -162,7 +162,10 @@ EXPORTED char *jmap_pointer_decode(const char *src, size_t len) return buf_release(&buf); } -EXPORTED json_t* jmap_patchobject_apply(json_t *val, json_t *patch, json_t *invalid) +EXPORTED json_t* jmap_patchobject_apply(json_t *val, + json_t *patch, + json_t *invalid, + unsigned flags) { const char *path; json_t *newval, *dst; @@ -173,33 +176,57 @@ EXPORTED json_t* jmap_patchobject_apply(json_t *val, json_t *patch, json_t *inva json_t *it = dst; const char *base = path, *top; /* Find path in object tree */ - while ((top = strchr(base, '/'))) { - char *name = jmap_pointer_decode(base, top-base); - if (json_is_array(it)) { - it = json_array_get(it, atoi(name)); + while (it && (top = strchr(base, '/'))) { + char *ref = jmap_pointer_decode(base, top-base); + if (json_is_array(it) && (flags & PATCH_ALLOW_ARRAY)) { + const char *err = NULL; + bit64 idx; + if (!parsenum(ref, &err, 0, &idx) && !*err && + idx < json_array_size(it)) { + it = json_array_get(it, idx); + } + else it = NULL; } else { - it = json_object_get(it, name); + it = json_object_get(it, ref); } - free(name); + free(ref); base = top + 1; } - if (!it) { - /* No such path in 'val' */ - if (invalid) { - json_array_append_new(invalid, json_string(path)); + + /* Set value at path */ + int is_valid = 0; + + if (it) { + char *ref = jmap_pointer_decode(base, strlen(base)); + + if (json_is_object(it)) { + is_valid = 1; + if (newval == json_null()) { + json_object_del(it, ref); + } else { + json_object_set(it, ref, newval); + } + } + else if (json_is_array(it) && !json_is_null(newval) && + (flags & PATCH_ALLOW_ARRAY)) { + const char *err = NULL; + bit64 idx; + if (!parsenum(ref, &err, 0, &idx) && !*err && + idx < json_array_size(it)) { + is_valid = 1; + json_array_set(it, idx, newval); + } } + + free(ref); + } + + if (!is_valid) { + if (invalid) json_array_append_new(invalid, json_string(path)); json_decref(dst); return NULL; } - /* Replace value at path */ - char *name = jmap_pointer_decode(base, strlen(base)); - if (newval == json_null()) { - json_object_del(it, name); - } else { - json_object_set(it, name, newval); - } - free(name); } return dst; diff --git a/imap/jmap_util.h b/imap/jmap_util.h index dd16033604e..8425ce81fef 100644 --- a/imap/jmap_util.h +++ b/imap/jmap_util.h @@ -65,13 +65,14 @@ extern int jmap_readprop_full(json_t *root, const char *prefix, const char *name int mandatory, json_t *invalid, const char *fmt, void *dst); +#define PATCH_NO_REMOVE (1<<0) // only relevant for create +#define PATCH_ALLOW_ARRAY (1<<1) + /* Apply patch to a deep copy of val and return the result. * Return NULL on error. If invalid is a JSON array, then * the erroneous path in patch is appended as JSON string */ -extern json_t* jmap_patchobject_apply(json_t *val, json_t *patch, json_t *invalid); - -#define PATCH_NO_REMOVE (1<<0) -#define PATCH_ALLOW_ARRAY (1<<1) +extern json_t* jmap_patchobject_apply(json_t *val, json_t *patch, + json_t *invalid, unsigned flags); /* Create a patch-object that transforms src into dst. */ extern json_t *jmap_patchobject_create(json_t *src, json_t *dst, unsigned flags);