From 806162c1c5b8334ff129fbeef9348f4589b871b5 Mon Sep 17 00:00:00 2001 From: Rexyysilent <285245987+Rexyysilent@users.noreply.github.com> Date: Thu, 22 Oct 2026 20:13:24 +0530 Subject: [PATCH] fix: reject container access through null JSON fields Initialize the root context before walking a nested JSON path. A null object member then reaches the existing type checks instead of replacing the entire root and discarding previously supplied request fields. Cover key, index, and append access in nested objects and top-level arrays, plus null values, direct overrides, and sparse array filling. Co-authored-by: Codex --- CHANGELOG.md | 4 ++++ httpie/cli/nested_json/interpret.py | 7 +++--- tests/test_json.py | 35 +++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0497ac3508..cb164b7218 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ This document records all notable changes to [HTTPie](https://httpie.io). This project adheres to [Semantic Versioning](https://semver.org/). +## Unreleased + +- Reject nested object or array access through a JSON `null` field with a type error instead of silently replacing the request body. Plain `null` values, direct value overrides, and filling sparse array entries remain supported. + ## [3.2.4](https://github.com/httpie/cli/compare/3.2.3...3.2.4) (2024-11-01) - Fix default certs loading and unpin `requests`. ([#1596](https://github.com/httpie/cli/issues/1596)) diff --git a/httpie/cli/nested_json/interpret.py b/httpie/cli/nested_json/interpret.py index 71fad98acd..0bef6544dc 100644 --- a/httpie/cli/nested_json/interpret.py +++ b/httpie/cli/nested_json/interpret.py @@ -65,10 +65,11 @@ def object_for(kind: PathAction) -> Any: else: assert_cant_happen() + # Initialize the root only; nested null values must pass type checking. + if context is None: + context = cursor = object_for(paths[0].kind) + for index, (path, next_path) in enumerate(zip(paths, paths[1:])): - # If there is no context yet, set it. - if cursor is None: - context = cursor = object_for(path.kind) if path.kind is PathAction.KEY: type_check(index, path, dict) if next_path.kind is PathAction.SET: diff --git a/tests/test_json.py b/tests/test_json.py index e758ebe7f4..334f994909 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -578,6 +578,41 @@ def test_nested_json_errors(input_json, expected_error, httpbin): assert expected_lines == exc_lines +@pytest.mark.parametrize('field', ['foo', 'foo[bar]', '[0][foo]']) +@pytest.mark.parametrize( + 'access, kind, required_type', + [('[key]', 'key', 'object'), ('[0]', 'index', 'array'), ('[]', 'append', 'array')], +) +def test_nested_json_null_container(field, access, kind, required_type): + with pytest.raises(NestedJSONSyntaxError) as exc: + http('--offline', DUMMY_URL, f'{field}:=null', f'{field}{access}=value') + + assert str(exc.value).splitlines()[0] == ( + f"HTTPie Type Error: Cannot perform {kind!r} based access on {field!r} " + "which has a type of 'NoneType' but this operation " + f"requires a type of {required_type!r}." + ) + + +@pytest.mark.parametrize( + 'input_json, expected_json', + [ + (['keep=value', 'foo:=null'], {'keep': 'value', 'foo': None}), + ( + ['keep=value', 'foo:=null', 'foo=value'], + {'keep': 'value', 'foo': 'value'}, + ), + ( + ['keep=value', 'foo[1]=last', 'foo[0][bar]=first'], + {'keep': 'value', 'foo': [{'bar': 'first'}, 'last']}, + ), + ], +) +def test_nested_json_null_values(input_json, expected_json): + r = http('--offline', DUMMY_URL, *input_json) + assert r.json == expected_json + + def test_nested_json_sparse_array(httpbin_both): r = http(httpbin_both + '/post', 'test[0]:=1', 'test[100]:=1') assert len(r.json['json']['test']) == 101