From 0389cc85ce058073430938f8f4824986caa4d663 Mon Sep 17 00:00:00 2001 From: Wleeaf <179125857+wleeaf@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:37:05 +0000 Subject: [PATCH] json_patch: reject a move past the end of the same array json_object_array_move_cb() added one to the array length when "from" and "path" are in the same array, so that the old length counted as a valid target index. A move is a remove followed by an add (RFC 6902 section 4.4), and the add's index must not be greater than the number of elements left after the remove. With the extra slot, json_object_array_insert_idx() was called one past the end and padded the array with a null: doc: {"foo": [1, 2, 3]} patch: [{"op": "move", "from": "/foo/0", "path": "/foo/3"}] returned 0 with {"foo": [2, 3, null, 1]}. It now fails with EINVAL, like an add to /foo/3 on a two-element array. Moving to /foo/2 or /foo/- still puts the element last. --- json_patch.c | 13 +++---------- tests/json_patch_tests.json | 10 ++++++++++ tests/test_json_patch.expected | 3 +++ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/json_patch.c b/json_patch.c index 0930816..d909d87 100644 --- a/json_patch.c +++ b/json_patch.c @@ -233,19 +233,12 @@ static int json_object_array_move_cb(struct json_object *parent, const char *key struct json_object *value, void *priv) { int rc; - struct json_pointer_get_result *from = priv; - size_t len = json_object_array_length(parent); /** - * If it's the same array parent, it means that we removed - * and element from it, so the length is temporarily reduced - * by 1, which means that if we try to move an element to - * the last position, we need to check the current length + 1 + * When moving within the same array, the element has already been + * removed, and idx refers to the shorter array (RFC 6902 section 4.4) */ - if (parent == from->parent) - len++; - - if (idx > len) + if (idx > json_object_array_length(parent)) { // Note: will propagate back out through json_pointer_set_with_cb() errno = EINVAL; diff --git a/tests/json_patch_tests.json b/tests/json_patch_tests.json index 5b9a41d..5a7f403 100644 --- a/tests/json_patch_tests.json +++ b/tests/json_patch_tests.json @@ -136,6 +136,16 @@ "doc": ["foo", "sil"], "patch": [{"op":"add", "path": "/3", "value": "bar"}], "error": "index is greater than number of items in array" }, + + { "comment": "Move within an array to the old array length is out of bounds", + "doc": {"foo": [1, 2, 3]}, + "patch": [{"op": "move", "from": "/foo/0", "path": "/foo/3"}], + "error": "/foo/3 is past the end of the array once /foo/0 is removed" }, + + { "comment": "Move within an array to its last index", + "doc": {"foo": [1, 2, 3]}, + "patch": [{"op": "move", "from": "/foo/0", "path": "/foo/2"}], + "expected": {"foo": [2, 3, 1]} }, { "comment": "test integer against equal double", "doc": {"n": 1}, diff --git a/tests/test_json_patch.expected b/tests/test_json_patch.expected index c4fe5f4..7191257 100644 --- a/tests/test_json_patch.expected +++ b/tests/test_json_patch.expected @@ -54,6 +54,9 @@ Testing '(null)', doc '[ "foo", "sil" ]' patch '[ { "op": "add", "path": "\/0", Testing 'push item to array via last index + 1', doc '[ "foo", "sil" ]' patch '[ { "op": "add", "path": "\/2", "value": "bar" } ]' : OK Testing 'add item to array at index > length should fail', doc '[ "foo", "sil" ]' patch '[ { "op": "add", "path": "\/3", "value": "bar" } ]' : OK => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Failed to set value at path referenced by 'path' field +Testing 'Move within an array to the old array length is out of bounds', doc '{ "foo": [ 1, 2, 3 ] }' patch '[ { "op": "move", "from": "\/foo\/0", "path": "\/foo\/3" } ]' : OK + => json_patch_apply failed as expected: ERRNO=EINVAL at patch idx 0: Failed to set value at path referenced by 'path' field +Testing 'Move within an array to its last index', doc '{ "foo": [ 1, 2, 3 ] }' patch '[ { "op": "move", "from": "\/foo\/0", "path": "\/foo\/2" } ]' : OK Testing 'test integer against equal double', doc '{ "n": 1 }' patch '[ { "op": "test", "path": "\/n", "value": 1.0 } ]' : OK Testing 'test double against equal integer', doc '{ "n": 1.0 }' patch '[ { "op": "test", "path": "\/n", "value": 1 } ]' : OK Testing 'test numeric equality inside arrays and objects', doc '{ "a": [ 1, { "n": -2.0 } ], "b": null }' patch '[ { "op": "test", "path": "", "value": { "b": null, "a": [ 1.0, { "n": -2 } ] } } ]' : OK