fix(865): fail closed on non-constructible typed scalars and bare indicators
Round-8: the PyYAML-absent conservative recognizer proved STRUCTURE and implicit-resolver TYPE but not CONSTRUCTOR VALIDITY. PyYAML safe_load raises a constructor ValueError on the WHOLE document when a plain scalar matches a typed implicit resolver but is not constructible (e.g. bad calendar date 2023-99-99, empty-radix ints 0b_/0x_), yielding no token; the fallback instead treated such a scalar as a null-equivalent, ignored the malformed key, and still emitted the valid login token -- a credential fail-open in the dangerous direction. _scalar now checks constructibility via _constructible (replicating PyYAML 6.0.3 construct_yaml_int/float/timestamp, stdlib-only): a typed scalar that is not constructor-valid returns _FAIL, which the parser turns into _Bail so the WHOLE document fails closed exactly as PyYAML does. int and timestamp resolver patterns are byte-identical to PyYAML's; the float pattern is a strict superset whose extras are all float()-constructible (fuzz-verified 0/400k raise), so it never fails closed where PyYAML would emit. Also fail closed on plain scalars beginning with an indicator a plain scalar may not start with: '%' (directive) and ',' (flow), and the conditional block indicators '-'/'?'/':' when followed by whitespace or end-of-value (bare sequence/complex-key/value indicators PyYAML rejects), while '-x'/'-1'/'?x'/':x' remain valid plain strings. Differential fuzz through the real function: 0 fail-opens across 1499 diverse non-tab cases; targeted fixtures assert fallback == PyYAML == fail-closed for 2023-99-99, 2023-13-01, bad-hour timestamp, 0b_, 0x_, 0x__, %broken, ',bad', bare '-'/'- '/'? key', and assert the token STILL resolves for constructible/ string look-alikes (valid date/datetime, 0o_, 4.e8, 0x1f, -x, :x). Both PyYAML present and forced-absent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -621,4 +621,102 @@ trailing: 1
|
||||
'
|
||||
assert_token "end marker then more content fails closed" "" primary git.example
|
||||
|
||||
# 16. CONSTRUCTOR-VALIDITY / INVALID-INDICATOR: a plain scalar can match a typed
|
||||
# implicit resolver (int/float/timestamp) yet be NON-constructible, or begin
|
||||
# with an indicator a plain scalar may not start with. PyYAML then RAISES on
|
||||
# the WHOLE document (constructor error / scanner error) and yields NO token,
|
||||
# so the fallback must ALSO fail closed for the whole document -- even though
|
||||
# the (unrelated) malformed key sits alongside an otherwise-valid logins
|
||||
# block whose token is itself well-formed. A prior residual proved STRUCTURE
|
||||
# and implicit TYPE but not constructor validity, so it ignored the malformed
|
||||
# key and still emitted the valid login token (fail-open in the dangerous
|
||||
# direction). assert_both_fail_closed asserts fallback == PyYAML == no token.
|
||||
assert_both_fail_closed() {
|
||||
local desc="$1" login="$2" host="$3" got
|
||||
got=$(token_fallback "$login" "$host")
|
||||
if [[ -n "$got" ]]; then
|
||||
echo "FAIL fallback [$desc]: expected fail-closed, got a token" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$HAVE_PYYAML" == true ]]; then
|
||||
got=$(token_pyyaml "$login" "$host")
|
||||
if [[ -n "$got" ]]; then
|
||||
echo "FAIL pyyaml [$desc]: expected PyYAML to also fail closed (raise/no token), got a token" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# write_bad_key_fixture: an unrelated root key carrying $1 as its plain scalar,
|
||||
# followed by an otherwise-valid logins block whose token is well-formed.
|
||||
write_bad_key_fixture() {
|
||||
write_fixture "bad: $1
|
||||
logins:
|
||||
- name: primary
|
||||
url: https://git.example
|
||||
token: TOK_PLAIN
|
||||
"
|
||||
}
|
||||
|
||||
# Non-constructible TIMESTAMP-tagged scalars: match the resolver, but the
|
||||
# calendar field is out of range so PyYAML's datetime construction raises.
|
||||
write_bad_key_fixture '2023-99-99' # month 99 / day 99 invalid
|
||||
assert_both_fail_closed "bad-date 2023-99-99 fails closed like PyYAML" primary git.example
|
||||
write_bad_key_fixture '2023-13-01' # month 13 invalid
|
||||
assert_both_fail_closed "bad-month 2023-13-01 fails closed like PyYAML" primary git.example
|
||||
write_bad_key_fixture '2023-01-15T25:00:00' # hour 25 invalid
|
||||
assert_both_fail_closed "bad-hour timestamp fails closed like PyYAML" primary git.example
|
||||
|
||||
# Non-constructible INT-tagged scalars: match the int resolver, but the radix
|
||||
# body is empty after underscore removal so int(base) raises.
|
||||
write_bad_key_fixture '0b_'
|
||||
assert_both_fail_closed "empty-binary 0b_ fails closed like PyYAML" primary git.example
|
||||
write_bad_key_fixture '0x_'
|
||||
assert_both_fail_closed "empty-hex 0x_ fails closed like PyYAML" primary git.example
|
||||
write_bad_key_fixture '0x__'
|
||||
assert_both_fail_closed "empty-hex 0x__ (multi-underscore) fails closed" primary git.example
|
||||
|
||||
# Invalid plain-scalar INDICATOR forms: a plain scalar may not begin with '%'
|
||||
# (directive) or ',' (flow) -- PyYAML raises a scanner/parser error on the whole
|
||||
# document, so the fallback fails closed on the leading indicator.
|
||||
write_bad_key_fixture '%broken'
|
||||
assert_both_fail_closed "leading-%% directive indicator fails closed" primary git.example
|
||||
write_bad_key_fixture ',bad'
|
||||
assert_both_fail_closed "leading-comma flow indicator fails closed" primary git.example
|
||||
# Bare block indicators in a value position ('-'/'- ', '?'/'? ', ':'/': '):
|
||||
# PyYAML raises a scanner error on the whole document, so the fallback must fail
|
||||
# closed rather than accept the indicator as a plain-scalar string.
|
||||
write_bad_key_fixture '-'
|
||||
assert_both_fail_closed "bare dash (seq indicator) fails closed" primary git.example
|
||||
write_bad_key_fixture '- x'
|
||||
assert_both_fail_closed "dash-space (seq entry) fails closed" primary git.example
|
||||
write_bad_key_fixture '? key'
|
||||
assert_both_fail_closed "question-space (complex key) fails closed" primary git.example
|
||||
# ...but an indicator NOT followed by whitespace is a valid plain scalar string,
|
||||
# so the token still resolves (no over-broad fail-close).
|
||||
write_bad_key_fixture '-x'
|
||||
assert_token "dash-not-space is a plain string, token resolves" "TOK_PLAIN" primary git.example
|
||||
write_bad_key_fixture ':x'
|
||||
assert_token "colon-not-space is a plain string, token resolves" "TOK_PLAIN" primary git.example
|
||||
|
||||
# NOT over-broad: a genuinely CONSTRUCTIBLE typed scalar (or a look-alike PyYAML
|
||||
# keeps as a plain string) leaves the document valid, so BOTH still resolve the
|
||||
# login token -- the fix must not fail closed on these.
|
||||
write_bad_key_fixture '2023-01-15'
|
||||
assert_token "valid date unrelated key still resolves token" "TOK_PLAIN" primary git.example
|
||||
write_bad_key_fixture '2023-01-15 10:00:00'
|
||||
assert_token "valid datetime unrelated key still resolves token" "TOK_PLAIN" primary git.example
|
||||
# '0o_' is NOT matched by PyYAML's int resolver (YAML 1.1 octal is 0[0-7]+, not
|
||||
# 0o...), so PyYAML keeps it a STRING and resolves the token; the fallback must
|
||||
# agree (no spurious fail-close).
|
||||
write_bad_key_fixture '0o_'
|
||||
assert_token "0o_ is a plain string in PyYAML, token still resolves" "TOK_PLAIN" primary git.example
|
||||
# '4.e8' matches the fallback's (superset) float pattern but PyYAML keeps it a
|
||||
# string; either way it is constructible, so the token still resolves in both.
|
||||
write_bad_key_fixture '4.e8'
|
||||
assert_token "4.e8 float look-alike still resolves token" "TOK_PLAIN" primary git.example
|
||||
# A valid radix int as an unrelated key must not fail closed.
|
||||
write_bad_key_fixture '0x1f'
|
||||
assert_token "valid hex int unrelated key still resolves token" "TOK_PLAIN" primary git.example
|
||||
|
||||
echo "Gitea login resolution regression harness passed"
|
||||
|
||||
Reference in New Issue
Block a user