From 75775a0b4e13379bed05ec499ab3bde788858e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AE=B6=E5=90=8D?= Date: Fri, 1 May 2026 20:38:01 +0800 Subject: [PATCH] fix: reject invalid json pointer escapes --- internal/binding/json_pointer.go | 29 ++++++++++++++++++++-- internal/binding/json_pointer_test.go | 35 +++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/internal/binding/json_pointer.go b/internal/binding/json_pointer.go index 2fecac110..e3f46e900 100644 --- a/internal/binding/json_pointer.go +++ b/internal/binding/json_pointer.go @@ -33,8 +33,10 @@ func ReadJSONPointer(data interface{}, pointer string) (interface{}, error) { for i, raw := range segments { // RFC 6901 unescaping: ~1 → /, ~0 → ~ (order matters). - key := strings.ReplaceAll(raw, "~1", "/") - key = strings.ReplaceAll(key, "~0", "~") + key, err := decodeJSONPointerSegment(raw) + if err != nil { + return nil, fmt.Errorf("json pointer %q: segment %q: %w", pointer, raw, err) + } m, ok := current.(map[string]interface{}) if !ok { @@ -53,3 +55,26 @@ func ReadJSONPointer(data interface{}, pointer string) (interface{}, error) { return current, nil } + +func decodeJSONPointerSegment(raw string) (string, error) { + var out strings.Builder + for i := 0; i < len(raw); i++ { + if raw[i] != '~' { + out.WriteByte(raw[i]) + continue + } + if i+1 >= len(raw) { + return "", fmt.Errorf("invalid escape: ~ must be followed by 0 or 1") + } + switch raw[i+1] { + case '0': + out.WriteByte('~') + case '1': + out.WriteByte('/') + default: + return "", fmt.Errorf("invalid escape: ~%c must be ~0 or ~1", raw[i+1]) + } + i++ + } + return out.String(), nil +} diff --git a/internal/binding/json_pointer_test.go b/internal/binding/json_pointer_test.go index 1fa415e05..26f0ec961 100644 --- a/internal/binding/json_pointer_test.go +++ b/internal/binding/json_pointer_test.go @@ -98,6 +98,41 @@ func TestReadJSONPointer_RFC6901_Escaping(t *testing.T) { } } +func TestReadJSONPointer_InvalidEscape(t *testing.T) { + data := map[string]interface{}{ + "a~2b": "literal", + "a~": "literal", + } + tests := []struct { + name string + pointer string + want string + }{ + { + name: "unsupported escape code", + pointer: "/a~2b", + want: `json pointer "/a~2b": segment "a~2b": invalid escape: ~2 must be ~0 or ~1`, + }, + { + name: "dangling tilde", + pointer: "/a~", + want: `json pointer "/a~": segment "a~": invalid escape: ~ must be followed by 0 or 1`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := ReadJSONPointer(data, tt.pointer) + if err == nil { + t.Fatal("expected error for invalid escape, got nil") + } + if err.Error() != tt.want { + t.Errorf("error = %q, want %q", err.Error(), tt.want) + } + }) + } +} + func TestReadJSONPointer_InvalidFormat(t *testing.T) { data := map[string]interface{}{"key": "val"} _, err := ReadJSONPointer(data, "no-leading-slash")