Two cases where encoding/json accepts input by quietly changing it, which is
the one thing a contract promising byte-identical agreement between peers
cannot tolerate.
Duplicate object properties. jsonschema.UnmarshalJSON keeps the last
occurrence, so {"fontSize":"small","fontSize":"large"} validated and stored
"large". Which one wins is a property of the parser, not of the contract: a
client generated against a different JSON library can disagree about what it
just sent, and the canonical form cannot represent the duplicate at all.
Lone surrogates. An unpaired \ud800 became U+FFFD and canonicalization
reported success, so the server would issue canonical bytes and an ETag for
an artifact a conforming implementation must refuse — RFC 8785 requires
terminating here. Substitution also means the value read back is not the
value written.
Both checks run before the decode that would hide them, on the shared
decodeJSON path that the manifest, its public projection and every value
schema go through, and again on the object branch of ValidateValue, which
uses a different decoder.
Reported by Codex review on #479.
167 lines
4.8 KiB
Go
167 lines
4.8 KiB
Go
package settingscontract
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"unicode/utf16"
|
|
)
|
|
|
|
// This file holds the two checks encoding/json will not make for us. Both cover
|
|
// input that Go accepts by silently changing it, which is the dangerous shape
|
|
// for a contract whose whole promise is that every peer agrees on the bytes.
|
|
|
|
// maxJSONDepth bounds the recursion in the duplicate-key scan. Setting values
|
|
// arrive from the network, and the deepest schema the contract declares nests
|
|
// three levels, so this is far above anything legitimate and still cannot be
|
|
// driven into a stack overflow.
|
|
const maxJSONDepth = 64
|
|
|
|
var errJSONTooDeep = fmt.Errorf("JSON nests deeper than %d levels", maxJSONDepth)
|
|
|
|
// rejectDuplicateKeys reports an error if any object in the document repeats a
|
|
// property name.
|
|
//
|
|
// encoding/json and the schema validator both keep the last occurrence, so
|
|
// {"fontSize":"small","fontSize":"large"} validates and stores "large" with no
|
|
// complaint. Which one wins is a property of the parser rather than of the
|
|
// contract: a client generated against a different JSON library can disagree
|
|
// about what it just sent, and the manifest's canonical form has no way to
|
|
// represent the duplicate at all. RFC 8785 leaves this to the caller, so the
|
|
// caller rejects it.
|
|
func rejectDuplicateKeys(raw []byte) error {
|
|
decoder := json.NewDecoder(bytes.NewReader(raw))
|
|
decoder.UseNumber()
|
|
token, err := decoder.Token()
|
|
if err != nil {
|
|
// Malformed JSON is the caller's problem to report, with its own
|
|
// message. Nothing here is a duplicate key.
|
|
return nil //nolint:nilerr // parse errors surface from the real decode
|
|
}
|
|
return scanDuplicateKeys(decoder, token, 0)
|
|
}
|
|
|
|
func scanDuplicateKeys(decoder *json.Decoder, token json.Token, depth int) error {
|
|
delim, ok := token.(json.Delim)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
if depth >= maxJSONDepth {
|
|
return errJSONTooDeep
|
|
}
|
|
|
|
switch delim {
|
|
case '{':
|
|
seen := map[string]struct{}{}
|
|
for decoder.More() {
|
|
nameToken, err := decoder.Token()
|
|
if err != nil {
|
|
return nil //nolint:nilerr // as above
|
|
}
|
|
name, ok := nameToken.(string)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
if _, duplicate := seen[name]; duplicate {
|
|
return fmt.Errorf("object repeats the property %q", name)
|
|
}
|
|
seen[name] = struct{}{}
|
|
|
|
valueToken, err := decoder.Token()
|
|
if err != nil {
|
|
return nil //nolint:nilerr // as above
|
|
}
|
|
if err := scanDuplicateKeys(decoder, valueToken, depth+1); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case '[':
|
|
for decoder.More() {
|
|
itemToken, err := decoder.Token()
|
|
if err != nil {
|
|
return nil //nolint:nilerr // as above
|
|
}
|
|
if err := scanDuplicateKeys(decoder, itemToken, depth+1); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Consume the closing delimiter so the caller resumes in the right place.
|
|
if _, err := decoder.Token(); err != nil {
|
|
return nil //nolint:nilerr // as above
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// rejectLoneSurrogates reports an error if any string literal contains a \u
|
|
// escape for an unpaired UTF-16 surrogate.
|
|
//
|
|
// encoding/json replaces one with U+FFFD and reports success, so the server
|
|
// would generate canonical bytes and an ETag for an artifact a conforming
|
|
// client must refuse: RFC 8785 requires canonicalization to terminate here.
|
|
// Silently substituting a replacement character also means the value read back
|
|
// is not the value written, which no setting should ever do.
|
|
func rejectLoneSurrogates(raw []byte) error {
|
|
inString := false
|
|
for i := 0; i < len(raw); i++ {
|
|
switch {
|
|
case !inString:
|
|
if raw[i] == '"' {
|
|
inString = true
|
|
}
|
|
case raw[i] == '"':
|
|
inString = false
|
|
case raw[i] == '\\':
|
|
if i+1 >= len(raw) {
|
|
return errors.New("string ends in an incomplete escape")
|
|
}
|
|
if raw[i+1] != 'u' {
|
|
// Any other two-character escape; skip both bytes.
|
|
i++
|
|
continue
|
|
}
|
|
code, err := parseHex4(raw, i+2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
i += 5
|
|
if !utf16.IsSurrogate(rune(code)) {
|
|
continue
|
|
}
|
|
if code >= 0xDC00 {
|
|
return fmt.Errorf(
|
|
"\\u%04X is a trailing surrogate with no leading surrogate before it", code)
|
|
}
|
|
// A leading surrogate must be followed immediately by a trailing one.
|
|
if i+6 >= len(raw) || raw[i+1] != '\\' || raw[i+2] != 'u' {
|
|
return fmt.Errorf(
|
|
"\\u%04X is a leading surrogate with no trailing surrogate after it", code)
|
|
}
|
|
low, err := parseHex4(raw, i+3)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if low < 0xDC00 || low > 0xDFFF {
|
|
return fmt.Errorf(
|
|
"\\u%04X is followed by \\u%04X, which is not a trailing surrogate", code, low)
|
|
}
|
|
i += 6
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseHex4(raw []byte, at int) (uint64, error) {
|
|
if at+4 > len(raw) {
|
|
return 0, errors.New("incomplete \\u escape")
|
|
}
|
|
code, err := strconv.ParseUint(string(raw[at:at+4]), 16, 32)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("malformed \\u escape %q", raw[at:at+4])
|
|
}
|
|
return code, nil
|
|
}
|