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.
This commit is contained in:
Wleeaf authored and Eric Hawicz committed 2026-11-09 08:22:21 -05:00
1 parent 46f0355925
commit 0389cc85ce
3 files changed
+16 -10

No files matched your search

+3 -10
View File
@@ -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;
+10
View File
@@ -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},
+3
View File
@@ -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