From 0d46ad0bb89432953c98472f28b29b2efedd19ec Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Mon, 22 Jun 2026 10:43:40 +0200 Subject: [PATCH] Reject JSON Pointer indexing into strings --- jsonpatch.py | 14 ++++++++++++-- tests.py | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/jsonpatch.py b/jsonpatch.py index d3fc26d..8119732 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -98,6 +98,12 @@ class JsonPatchTestFailed(JsonPatchException, AssertionError): """ A Test operation failed """ +def _raise_if_string_index(document, part): + if isinstance(document, basestring): + raise JsonPointerException( + "unable to resolve json pointer against string value, part {0}".format(part)) + + def multidict(ordered_pairs): """Convert duplicate keys values to lists.""" # read all values into lists @@ -240,6 +246,7 @@ class RemoveOperation(PatchOperation): def apply(self, obj): subobj, part = self.pointer.to_last(obj) + _raise_if_string_index(subobj, part) if isinstance(subobj, Sequence) and not isinstance(part, int): raise JsonPointerException("invalid array index '{0}'".format(part)) @@ -378,8 +385,9 @@ def apply(self, obj): subobj, part = from_ptr.to_last(obj) try: + _raise_if_string_index(subobj, part) value = subobj[part] - except (KeyError, IndexError) as ex: + except (KeyError, IndexError, JsonPointerException) as ex: raise JsonPatchConflict(str(ex)) # If source and target are equal, this is a no-op @@ -458,6 +466,7 @@ def apply(self, obj): if part is None: val = subobj else: + _raise_if_string_index(subobj, part) val = self.pointer.walk(subobj, part) except JsonPointerException as ex: raise JsonPatchTestFailed(str(ex)) @@ -488,8 +497,9 @@ def apply(self, obj): subobj, part = from_ptr.to_last(obj) try: + _raise_if_string_index(subobj, part) value = copy.deepcopy(subobj[part]) - except (KeyError, IndexError) as ex: + except (KeyError, IndexError, JsonPointerException) as ex: raise JsonPatchConflict(str(ex)) obj = AddOperation({ diff --git a/tests.py b/tests.py index d9eea92..c0c5f3a 100755 --- a/tests.py +++ b/tests.py @@ -87,6 +87,12 @@ def test_remove_array_item(self): res = jsonpatch.apply_patch(obj, [{'op': 'remove', 'path': '/foo/1'}]) self.assertEqual(res['foo'], ['bar', 'baz']) + def test_remove_does_not_index_string(self): + obj = {'foo': 'should-not-be-indexable'} + patch_obj = [{'op': 'remove', 'path': '/foo/0'}] + self.assertRaises(jsonpointer.JsonPointerException, + jsonpatch.apply_patch, obj, patch_obj) + def test_remove_invalid_item(self): obj = {'foo': ['bar', 'qux', 'baz']} with self.assertRaises(jsonpointer.JsonPointerException): @@ -134,6 +140,12 @@ def test_move_array_item(self): res = jsonpatch.apply_patch(obj, [{'op': 'move', 'from': '/foo/1', 'path': '/foo/3'}]) self.assertEqual(res, {'foo': ['all', 'cows', 'eat', 'grass']}) + def test_move_does_not_index_string(self): + obj = {'foo': 'should-not-be-indexable', 'bar': []} + patch_obj = [{'op': 'move', 'from': '/foo/0', 'path': '/bar/0'}] + self.assertRaises(jsonpatch.JsonPatchConflict, + jsonpatch.apply_patch, obj, patch_obj) + def test_move_array_item_into_other_item(self): obj = [{"foo": []}, {"bar": []}] patch = [{"op": "move", "from": "/0", "path": "/0/bar/0"}] @@ -159,6 +171,12 @@ def test_copy_array_item(self): res = jsonpatch.apply_patch(obj, [{'op': 'copy', 'from': '/foo/1', 'path': '/foo/3'}]) self.assertEqual(res, {'foo': ['all', 'grass', 'cows', 'grass', 'eat']}) + def test_copy_does_not_index_string(self): + obj = {'foo': 'should-not-be-indexable'} + patch_obj = [{'op': 'copy', 'from': '/foo/0', 'path': '/bar'}] + self.assertRaises(jsonpatch.JsonPatchConflict, + jsonpatch.apply_patch, obj, patch_obj) + def test_copy_mutable(self): """ test if mutable objects (dicts and lists) are copied by value """ @@ -177,6 +195,12 @@ def test_test_success(self): jsonpatch.apply_patch(obj, [{'op': 'test', 'path': '/baz', 'value': 'qux'}, {'op': 'test', 'path': '/foo/1', 'value': 2}]) + def test_test_does_not_index_string(self): + obj = {'foo': 'should-not-be-indexable'} + patch_obj = [{'op': 'test', 'path': '/foo/0', 'value': 's'}] + self.assertRaises(jsonpatch.JsonPatchTestFailed, + jsonpatch.apply_patch, obj, patch_obj) + def test_test_whole_obj(self): obj = {'baz': 1} jsonpatch.apply_patch(obj, [{'op': 'test', 'path': '', 'value': obj}])