fix(git): fail closed on forbidden control chars, fix float exponent + '?' over-rejection (#865)
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
Round-9 auditor blockers, both fixed at root cause in the PyYAML-absent
line-parser fallback of get_gitea_token_for_login:
1. Control-character fail-open (dangerous): PyYAML 6.0.3's Reader rejects
the WHOLE document (ReaderError) if a forbidden C0/DEL control byte
{0x00-0x08, 0x0B, 0x0C, 0x0E-0x1F, 0x7F} appears ANYWHERE in the raw
stream -- plain scalar, inside quotes, or a comment -- regardless of
position, verified empirically against the real installed PyYAML 6.0.3.
The fallback previously only guarded tabs and emitted the login token
from such documents. Added a whole-document _FORBIDDEN_CONTROL scan on
the raw text (before splitlines(), which itself splits on some of the
same bytes) so the fallback fails closed identically to PyYAML.
2. Unsigned-exponent float over-rejection: PyYAML's implicit float
resolver requires an EXPLICIT sign on the exponent; unsigned-exponent
spellings (1.0e10, +1.0e10, -1.0e10, 1.0E10, .5e10, 4.e8) are PyYAML
STRINGS, not floats. The fallback's _IMPLICIT_FLOAT pattern allowed an
optional sign, misclassifying these as floats and dropping the token.
Tightened the regex to match PyYAML's resolver exactly.
Also folds in a residual over-rejection found via this round's wide
differential fuzz (1767 cases, 0 fail-opens / 0 over-rejections after the
fix): a plain scalar starting with "?" not followed by whitespace (e.g.
"?x") is a valid PyYAML string, but the fallback's blanket-reject set
treated every leading "?" as illegal. Narrowed to match the existing
space-aware handling already used for "-" and ":".
Adds regression fixtures to test-gitea-login-resolution.sh covering all
three fixes under both PyYAML-present and forced-ImportError-absent runs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -776,4 +776,121 @@ write_fixture "logins:
|
||||
"
|
||||
assert_token "quoted token with inner tab resolves verbatim" "T${TAB}OK" primary git.example
|
||||
|
||||
# 18. CONTROL-CHARACTER FAIL-CLOSE (#865 round-9 blocker 1): PyYAML's Reader
|
||||
# scans the ENTIRE raw document stream (not merely scalar contents, and NOT
|
||||
# scoped by quoting) for bytes outside its printable set and raises
|
||||
# ReaderError -- a WHOLE-DOCUMENT reject -- the instant one is found,
|
||||
# regardless of where it sits: an unrelated field's plain scalar, inside a
|
||||
# double- or single-quoted scalar, or a comment. Verified empirically against
|
||||
# real installed PyYAML 6.0.3 (see detect-platform.sh's _FORBIDDEN_CONTROL
|
||||
# comment): every C0 control byte {0x00-0x08, 0x0B, 0x0C, 0x0E-0x1F} plus DEL
|
||||
# (0x7F) rejects in ALL THREE contexts (plain / double-quoted / single-quoted);
|
||||
# only TAB(0x09), LF(0x0A), CR(0x0D) are accepted among the low byte range
|
||||
# (TAB has its own narrower, position-aware coverage in section 17 above; LF/CR
|
||||
# are line separators). A prior fallback ONLY guarded tabs and emitted the
|
||||
# login token from documents PyYAML rejects over an UNRELATED field's control
|
||||
# byte -- a dangerous fail-open (credential emission from a document PyYAML
|
||||
# refuses). write_control_char_fixture writes the raw byte directly via
|
||||
# printf's octal escape (never through a bash string/variable, which cannot
|
||||
# hold an embedded NUL) so 0x00 is exercised faithfully alongside the rest.
|
||||
write_control_char_fixture() {
|
||||
local octal="$1" quote="${2:-}"
|
||||
{
|
||||
if [[ -n "$quote" ]]; then
|
||||
printf 'bad: %sx' "$quote"
|
||||
# shellcheck disable=SC2059 # deliberate: $octal supplies printf's
|
||||
# own \NNN octal escape so the raw control byte reaches the file
|
||||
# directly, never passing through a bash string (which truncates
|
||||
# at an embedded NUL and so cannot represent byte 0x00 otherwise).
|
||||
printf "\\${octal}"
|
||||
printf 'y%s\n' "$quote"
|
||||
else
|
||||
printf 'bad: x'
|
||||
# shellcheck disable=SC2059 # deliberate: $octal supplies printf's
|
||||
# own \NNN octal escape so the raw control byte reaches the file
|
||||
# directly, never passing through a bash string (which truncates
|
||||
# at an embedded NUL and so cannot represent byte 0x00 otherwise).
|
||||
printf "\\${octal}"
|
||||
printf 'y\n'
|
||||
fi
|
||||
printf 'logins:\n - name: primary\n url: https://git.example\n token: TOK_PLAIN\n'
|
||||
} > "$FIXTURE_XDG/tea/config.yml"
|
||||
}
|
||||
|
||||
# Plain (unquoted) unrelated-field placement: the full empirically-confirmed
|
||||
# forbidden C0/DEL set.
|
||||
for octal in 000 001 002 003 004 005 006 007 010 013 014 \
|
||||
016 017 020 021 022 023 024 025 026 027 \
|
||||
030 031 032 033 034 035 036 037 177; do
|
||||
write_control_char_fixture "$octal"
|
||||
assert_both_fail_closed "control byte \\$octal in unrelated plain field fails closed" primary git.example
|
||||
done
|
||||
|
||||
# Inside-quote variants (double and single) for the six bytes called out
|
||||
# explicitly in the round-9 blocker report: 0x00,0x01,0x07,0x0e,0x1f,0x7f.
|
||||
for octal in 000 001 007 016 037 177; do
|
||||
write_control_char_fixture "$octal" '"'
|
||||
assert_both_fail_closed "control byte \\$octal inside double-quoted unrelated field fails closed" primary git.example
|
||||
write_control_char_fixture "$octal" "'"
|
||||
assert_both_fail_closed "control byte \\$octal inside single-quoted unrelated field fails closed" primary git.example
|
||||
done
|
||||
|
||||
# Same forbidden byte inside a comment line -- PyYAML's Reader check is
|
||||
# stream-wide, so it rejects here too, not merely inside live scalar content.
|
||||
{
|
||||
printf '# note'
|
||||
printf '\007'
|
||||
printf 'here\nlogins:\n - name: primary\n url: https://git.example\n token: TOK_PLAIN\n'
|
||||
} > "$FIXTURE_XDG/tea/config.yml"
|
||||
assert_both_fail_closed "control byte in a comment line fails closed" primary git.example
|
||||
|
||||
# NOT over-broad: TAB/LF/CR remain accepted where PyYAML already accepts them
|
||||
# (covered by section 17's tab fixtures and the ordinary newline-delimited
|
||||
# fixtures used throughout this file), so no additional assertion is needed
|
||||
# here beyond confirming the forbidden-control guard does not fire on them.
|
||||
|
||||
# 19. UNSIGNED-EXPONENT FLOAT OVER-REJECTION (#865 round-9 blocker 2): PyYAML
|
||||
# 6.0.3's implicit float resolver requires an EXPLICIT SIGN on the exponent
|
||||
# ([eE][-+][0-9]+); an unsigned exponent is NOT matched, so PyYAML resolves
|
||||
# the scalar as a plain STRING, not a float. A prior fallback's float
|
||||
# recognizer accepted an OPTIONAL sign ([eE][-+]?[0-9]+), over-matching these
|
||||
# spellings as floats and dropping the token PyYAML would emit verbatim
|
||||
# (over-rejection). Verified empirically against real PyYAML 6.0.3.
|
||||
for form in '1.0e10' '+1.0e10' '-1.0e10' '1.0E10' '.5e10' '4.e8'; do
|
||||
write_fixture "logins:
|
||||
- name: primary
|
||||
url: https://git.example
|
||||
token: ${form}
|
||||
"
|
||||
assert_token "unsigned-exponent form '$form' is a PyYAML string, token resolves" "$form" primary git.example
|
||||
done
|
||||
|
||||
# Parity guard: a genuine SIGNED-exponent float is still typed as a non-string
|
||||
# float by PyYAML and must still fail closed (not regress into over-acceptance).
|
||||
write_fixture 'logins:
|
||||
- name: primary
|
||||
url: https://git.example
|
||||
token: 1.0e+10
|
||||
'
|
||||
assert_token "signed-exponent genuine float still fails closed" "" primary git.example
|
||||
write_fixture 'logins:
|
||||
- name: primary
|
||||
url: https://git.example
|
||||
token: 1.0e-10
|
||||
'
|
||||
assert_token "signed-exponent (negative) genuine float still fails closed" "" primary git.example
|
||||
|
||||
# 20. RESIDUAL OVER-REJECTION found via round-9 differential fuzzing (folded into
|
||||
# this round, not split off): a plain scalar starting with "?" NOT followed by
|
||||
# whitespace (e.g. "?x") is a valid PyYAML string -- only a bare "?" or "? "
|
||||
# (question mark followed by space/EOL) opens a complex mapping key and is
|
||||
# illegal in a value position. A prior blanket-reject set treated EVERY
|
||||
# leading "?" as illegal, over-rejecting a token PyYAML accepts verbatim.
|
||||
write_fixture 'logins:
|
||||
- name: primary
|
||||
url: https://git.example
|
||||
token: ?x
|
||||
'
|
||||
assert_token "question-mark-not-space is a plain string, token resolves" "?x" primary git.example
|
||||
|
||||
echo "Gitea login resolution regression harness passed"
|
||||
|
||||
Reference in New Issue
Block a user