All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
Round-10 auditor blockers in the PyYAML-absent fallback of
get_gitea_token_for_login (detect-platform.sh):
Blocker 1 (fail-open): the round-9 control-char guard used a hand-rolled
C0/DEL subset that missed code points PyYAML's Reader also rejects -- the
C1 block (U+0080-0084, U+0086-009F), the surrogate range, and the BMP
noncharacters U+FFFE/U+FFFF -- so the fallback still emitted a token from
documents PyYAML rejects whole. _FORBIDDEN_CONTROL now uses PyYAML 6.0.3's
EXACT Reader.NON_PRINTABLE character class (negated), verified 0-mismatch
against real PyYAML across the full BMP + astral range.
Blocker 2 (over-reject): the recognizer blanket-rejected any plain scalar
containing a flow indicator, but in BLOCK context (where a tea config
value always sits) PyYAML treats ',[]{}' as ordinary content and treats
'!&*|>#%@`' and quotes as significant only at the first non-space char.
The blanket scan dropped tokens PyYAML emits verbatim (internal '!', ','
'[' ']' '{' '}' '#'-not-space-preceded, ':'-not-space-followed). Removed
it; the leading-char guard and the ' #' / ': ' / trailing-':' guards keep
the fail-open direction shut.
Tests: additions-only sections 21 (printable-boundary fail-close for C1 +
noncharacters; NEL/U+00A0/astral still resolve) and 22 (internal
indicators resolve; leading indicator / ': ' inline map / trailing ':'
fail closed). Verified vs real PyYAML 6.0.3 both directions (present as
oracle, absent via PYTHONPATH shadow): wide 602 cases 0 fail-open/0
present-mismatch, dense 712 cases 0 fail-open/0 over-reject/0 mismatch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1011 lines
43 KiB
Bash
Executable File
1011 lines
43 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Regression harness for host-specific Gitea tea login resolution.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/gitea-login-resolution}"
|
|
REPO_DIR="$WORK_DIR/repo"
|
|
BIN_DIR="$WORK_DIR/bin"
|
|
LOG_FILE="$WORK_DIR/calls.log"
|
|
CREDENTIALS_FILE="$WORK_DIR/credentials.json"
|
|
|
|
rm -rf "$WORK_DIR"
|
|
mkdir -p "$REPO_DIR" "$BIN_DIR"
|
|
|
|
git -C "$REPO_DIR" init -q
|
|
git -C "$REPO_DIR" remote add origin https://git.uscllc.com/USC/uconnect.git
|
|
|
|
cat > "$CREDENTIALS_FILE" <<'JSON'
|
|
{
|
|
"gitea": {
|
|
"mosaicstack": {
|
|
"url": "https://git.mosaicstack.dev",
|
|
"token": "mosaic-token"
|
|
},
|
|
"usc": {
|
|
"url": "https://git.uscllc.com",
|
|
"token": "usc-token"
|
|
}
|
|
}
|
|
}
|
|
JSON
|
|
|
|
cat > "$BIN_DIR/tea" <<'SH'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
if [[ "$*" == "login list --output json" ]]; then
|
|
cat <<'JSON'
|
|
[
|
|
{"name":"evil-usc","url":"https://evilgit.uscllc.com","user":"bad.actor"},
|
|
{"name":"usc","url":"https://git.uscllc.com","user":"ci-bot"}
|
|
]
|
|
JSON
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "${1:-}" == "api" ]]; then
|
|
printf '%s\n' '{"login":"ci-bot"}'
|
|
exit 0
|
|
fi
|
|
|
|
printf 'tea %s\n' "$*" >> "$MOSAIC_TEST_LOG"
|
|
if [[ "${MOSAIC_TEA_FAIL_PR_CREATE:-}" == "1" && "$*" == pr\ create* ]]; then
|
|
echo 'GetUserByName: simulated stale login failure' >&2
|
|
exit 1
|
|
fi
|
|
exit 0
|
|
SH
|
|
|
|
cat > "$BIN_DIR/curl" <<'SH'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
printf 'curl %s\n' "$*" >> "$MOSAIC_TEST_LOG"
|
|
url="${*: -1}"
|
|
case "$url" in
|
|
*/pulls/*.diff)
|
|
printf 'diff --git a/file b/file\n'
|
|
;;
|
|
*/pulls/*)
|
|
printf '{"head":{"sha":"abc123"}}'
|
|
;;
|
|
*/commits/*/status)
|
|
printf '{"state":"success","statuses":[{"context":"ci/mock","status":"success"}]}'
|
|
;;
|
|
*)
|
|
printf '{}'
|
|
;;
|
|
esac
|
|
SH
|
|
|
|
chmod +x "$BIN_DIR/tea" "$BIN_DIR/curl"
|
|
|
|
run_in_repo() {
|
|
(
|
|
cd "$REPO_DIR"
|
|
PATH="$BIN_DIR:$PATH" \
|
|
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
|
MOSAIC_TEST_LOG="$LOG_FILE" \
|
|
"$@"
|
|
)
|
|
}
|
|
|
|
usc_login=$(run_in_repo bash -c '
|
|
export GITEA_LOGIN=mosaicstack
|
|
export GITEA_URL=https://git.mosaicstack.dev
|
|
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
|
get_gitea_login
|
|
')
|
|
if [[ "$usc_login" != "usc" ]]; then
|
|
echo "Expected USC host to resolve tea login 'usc' despite stale mosaicstack env; got '$usc_login'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
usc_login_with_usc_url=$(run_in_repo bash -c '
|
|
export GITEA_LOGIN=mosaicstack
|
|
export GITEA_URL=https://git.uscllc.com
|
|
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
|
get_gitea_login
|
|
')
|
|
if [[ "$usc_login_with_usc_url" != "usc" ]]; then
|
|
echo "Expected USC host to reject stale GITEA_LOGIN even when GITEA_URL matches USC; got '$usc_login_with_usc_url'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
usc_login_without_url=$(run_in_repo bash -c '
|
|
export GITEA_LOGIN=mosaicstack
|
|
unset GITEA_URL
|
|
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
|
get_gitea_login
|
|
')
|
|
if [[ "$usc_login_without_url" != "usc" ]]; then
|
|
echo "Expected USC host to ignore unmatched GITEA_LOGIN without URL; got '$usc_login_without_url'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
git -C "$REPO_DIR" remote set-url origin https://hermes:token@git.uscllc.com/USC/uconnect.git
|
|
embedded_host=$(run_in_repo bash -c '
|
|
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
|
get_remote_host
|
|
')
|
|
if [[ "$embedded_host" != "git.uscllc.com" ]]; then
|
|
echo "Expected credential-bearing remote host to strip userinfo; got '$embedded_host'" >&2
|
|
exit 1
|
|
fi
|
|
git -C "$REPO_DIR" remote set-url origin https://git.uscllc.com/USC/uconnect.git
|
|
|
|
override_login=$(run_in_repo bash -c '
|
|
export GITEA_LOGIN=usc
|
|
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
|
get_gitea_login_for_repo_override
|
|
')
|
|
if [[ "$override_login" != "usc" ]]; then
|
|
echo "Expected --repo override path to honor explicit GITEA_LOGIN; got '$override_login'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
git -C "$REPO_DIR" remote set-url origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
|
: > "$LOG_FILE"
|
|
run_in_repo env GITEA_LOGIN=usc "$SCRIPT_DIR/issue-list.sh" --repo USC/uconnect -n 1
|
|
grep -q -- 'tea issues list --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
git -C "$REPO_DIR" remote set-url origin https://git.uscllc.com/USC/uconnect.git
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo "$SCRIPT_DIR/issue-close.sh" -i 42
|
|
grep -q -- 'tea issue close 42 --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
if grep -q -- '--login mosaicstack' "$LOG_FILE"; then
|
|
echo "issue-close.sh used hardcoded mosaicstack login on USC host" >&2
|
|
exit 1
|
|
fi
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo "$SCRIPT_DIR/milestone-list.sh"
|
|
grep -q -- 'tea milestone list --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo "$SCRIPT_DIR/milestone-create.sh" -t "0.2.0" -d "USC milestone"
|
|
grep -q -- 'tea milestones create --title 0.2.0 --description USC milestone --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo "$SCRIPT_DIR/milestone-close.sh" -t "0.2.0"
|
|
grep -q -- 'tea milestone close 0.2.0 --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
|
|
if command -v pwsh >/dev/null 2>&1; then
|
|
: > "$LOG_FILE"
|
|
run_in_repo pwsh -NoProfile -File "$SCRIPT_DIR/issue-list.ps1" -Limit 1
|
|
grep -q -- 'tea issues list --state open --limit 1 --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo pwsh -NoProfile -File "$SCRIPT_DIR/issue-create.ps1" -Title "PowerShell issue"
|
|
grep -q -- 'tea issue create --title PowerShell issue --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo pwsh -NoProfile -File "$SCRIPT_DIR/pr-list.ps1" -Limit 1
|
|
grep -q -- 'tea pr list --state open --limit 1 --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo pwsh -NoProfile -File "$SCRIPT_DIR/pr-create.ps1" -Title "PowerShell PR"
|
|
grep -q -- 'tea pr create --title PowerShell PR --head master --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo pwsh -NoProfile -File "$SCRIPT_DIR/pr-merge.ps1" -Number 42 -SkipQueueGuard
|
|
grep -q -- 'tea pr merge 42 --style squash --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo pwsh -NoProfile -File "$SCRIPT_DIR/milestone-create.ps1" -List
|
|
grep -q -- 'tea milestones list --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
fi
|
|
|
|
: > "$LOG_FILE"
|
|
if run_in_repo "$SCRIPT_DIR/pr-diff.sh" --repo USC/uconnect -n 7 >/dev/null 2>&1; then
|
|
echo "Expected pr-diff.sh --repo without host to fail loud" >&2
|
|
exit 1
|
|
fi
|
|
if grep -q -- 'git.mosaicstack.dev/api/v1/repos/USC/uconnect' "$LOG_FILE"; then
|
|
echo "pr-diff.sh --repo defaulted API host to git.mosaicstack.dev" >&2
|
|
exit 1
|
|
fi
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo env GITEA_URL=https://git.uscllc.com "$SCRIPT_DIR/pr-diff.sh" --repo USC/uconnect -n 7 >/dev/null
|
|
grep -q -- 'curl .*https://git.uscllc.com/api/v1/repos/USC/uconnect/pulls/7.diff' "$LOG_FILE"
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo "$SCRIPT_DIR/pr-ci-wait.sh" --repo USC/uconnect --host git.uscllc.com -n 9 -t 2 -i 1
|
|
grep -q -- 'curl .*https://git.uscllc.com/api/v1/repos/USC/uconnect/pulls/9' "$LOG_FILE"
|
|
grep -q -- 'curl .*https://git.uscllc.com/api/v1/repos/USC/uconnect/commits/abc123/status' "$LOG_FILE"
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo env MOSAIC_TEA_FAIL_PR_CREATE=1 GITEA_TOKEN=usc-token GITEA_URL=https://git.uscllc.com "$SCRIPT_DIR/pr-create.sh" -t "USC API fallback" -H feature/pr-create
|
|
grep -q -- 'tea pr create --repo USC/uconnect --login usc --title USC API fallback --head feature/pr-create' "$LOG_FILE"
|
|
grep -q -- 'curl .*Authorization: token usc-token .*https://git.uscllc.com/api/v1/repos/USC/uconnect/pulls' "$LOG_FILE"
|
|
if grep -q -- 'git.mosaicstack.dev/api/v1/repos/USC/uconnect/pulls' "$LOG_FILE"; then
|
|
echo "pr-create.sh API fallback defaulted USC repo to git.mosaicstack.dev" >&2
|
|
exit 1
|
|
fi
|
|
|
|
git -C "$REPO_DIR" remote set-url origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
|
: > "$LOG_FILE"
|
|
run_in_repo env GITEA_TOKEN=mosaic-token GITEA_URL=https://git.mosaicstack.dev "$SCRIPT_DIR/issue-close.sh" -i 536
|
|
grep -q -- 'curl .*https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/536' "$LOG_FILE"
|
|
if grep -q -- 'tea issue close 536 .*--login mosaicstack' "$LOG_FILE"; then
|
|
echo "issue-close.sh invented a mosaicstack tea login instead of using API fallback" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# #560: loud diagnostic + host-derived login for BOTH instances + override-wins
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Loud diagnostic: a host with no matching tea login must emit an actionable
|
|
# error to stderr (the previous behavior was a SILENT failure). The original
|
|
# mock defines only usc/evil-usc logins, so mosaicstack resolution fails here.
|
|
git -C "$REPO_DIR" remote set-url origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
|
diag_stderr=$(run_in_repo bash -c '
|
|
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
|
get_gitea_login_for_host git.mosaicstack.dev
|
|
' 2>&1 1>/dev/null || true)
|
|
if ! grep -q "no Gitea tea login matches host 'git.mosaicstack.dev'" <<<"$diag_stderr"; then
|
|
echo "Expected loud diagnostic naming the unresolved host; got: $diag_stderr" >&2
|
|
exit 1
|
|
fi
|
|
if ! grep -q "Available tea logins:" <<<"$diag_stderr"; then
|
|
echo "Expected diagnostic to list available tea logins; got: $diag_stderr" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Both-instance host derivation + override-wins, using a mock that DOES define a
|
|
# mosaicstack login. Scoped to this section so the API-fallback assertions above
|
|
# (which rely on mosaicstack having NO tea login) remain valid.
|
|
BIN_DIR2="$WORK_DIR/bin2"
|
|
mkdir -p "$BIN_DIR2"
|
|
cp "$BIN_DIR/curl" "$BIN_DIR2/curl"
|
|
cat > "$BIN_DIR2/tea" <<'SH'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
if [[ "$*" == "login list --output json" ]]; then
|
|
cat <<'JSON'
|
|
[
|
|
{"name":"mosaicstack","url":"https://git.mosaicstack.dev","user":"ci-bot"},
|
|
{"name":"usc","url":"https://git.uscllc.com","user":"ci-bot"}
|
|
]
|
|
JSON
|
|
exit 0
|
|
fi
|
|
printf 'tea %s\n' "$*" >> "$MOSAIC_TEST_LOG"
|
|
exit 0
|
|
SH
|
|
chmod +x "$BIN_DIR2/tea"
|
|
|
|
run_in_repo2() {
|
|
(
|
|
cd "$REPO_DIR"
|
|
PATH="$BIN_DIR2:$PATH" \
|
|
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
|
MOSAIC_TEST_LOG="$LOG_FILE" \
|
|
"$@"
|
|
)
|
|
}
|
|
|
|
git -C "$REPO_DIR" remote set-url origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
|
mosaic_login=$(run_in_repo2 bash -c 'source "'"$SCRIPT_DIR"'/detect-platform.sh"; get_gitea_login')
|
|
if [[ "$mosaic_login" != "mosaicstack" ]]; then
|
|
echo "Expected mosaicstack origin to derive login 'mosaicstack'; got '$mosaic_login'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
git -C "$REPO_DIR" remote set-url origin https://git.uscllc.com/USC/uconnect.git
|
|
usc_login_derived=$(run_in_repo2 bash -c 'source "'"$SCRIPT_DIR"'/detect-platform.sh"; get_gitea_login')
|
|
if [[ "$usc_login_derived" != "usc" ]]; then
|
|
echo "Expected usc origin to derive login 'usc'; got '$usc_login_derived'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Explicit GITEA_LOGIN override is honored when it matches the host.
|
|
git -C "$REPO_DIR" remote set-url origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
|
override_wins=$(run_in_repo2 bash -c 'export GITEA_LOGIN=mosaicstack; source "'"$SCRIPT_DIR"'/detect-platform.sh"; get_gitea_login')
|
|
if [[ "$override_wins" != "mosaicstack" ]]; then
|
|
echo "Expected valid GITEA_LOGIN override to win on mosaicstack host; got '$override_wins'" >&2
|
|
exit 1
|
|
fi
|
|
git -C "$REPO_DIR" remote set-url origin https://git.uscllc.com/USC/uconnect.git
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# #865 Blocker 1 & 2: get_gitea_token_for_login must resolve the SAME token as
|
|
# PyYAML would (or fail closed identically) even when PyYAML is ABSENT, and must
|
|
# bind the credential to the repo host's scheme + host + EFFECTIVE PORT — not the
|
|
# hostname alone. These fixtures probe the ImportError-dispatched line-parser
|
|
# fallback under FORCED PyYAML absence with adversarial YAML shapes, asserting it
|
|
# NEVER misattributes a token from a nested sub-map or a mis-indented line, strips
|
|
# inline comments like PyYAML, fails closed where PyYAML errors, and rejects a
|
|
# port mismatch while accepting an exact / default-port match. When PyYAML is
|
|
# available the same fixtures also assert the PyYAML path agrees (equivalence).
|
|
# ---------------------------------------------------------------------------
|
|
FIXTURE_XDG="$WORK_DIR/tokenfix"
|
|
NOYAML_DIR="$WORK_DIR/noyaml"
|
|
mkdir -p "$FIXTURE_XDG/tea" "$NOYAML_DIR"
|
|
# A shadow `yaml` module that raises ImportError, forcing the fallback path.
|
|
printf 'raise ImportError("forced-absent for #865 fallback regression")\n' > "$NOYAML_DIR/yaml.py"
|
|
if python3 -c 'import yaml' >/dev/null 2>&1; then HAVE_PYYAML=true; else HAVE_PYYAML=false; fi
|
|
# Confirm the shim really does force ImportError, so the fallback is exercised.
|
|
if python3 -c 'import yaml' >/dev/null 2>&1; then
|
|
if PYTHONPATH="$NOYAML_DIR" python3 -c 'import yaml' >/dev/null 2>&1; then
|
|
echo "FAIL: PyYAML-absence shim did not force ImportError (fallback not exercised)" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
write_fixture() { printf '%s' "$1" > "$FIXTURE_XDG/tea/config.yml"; }
|
|
|
|
# Resolve a token via the FORCED-fallback path (PyYAML shimmed to ImportError).
|
|
token_fallback() {
|
|
(
|
|
cd "$REPO_DIR"
|
|
XDG_CONFIG_HOME="$FIXTURE_XDG" PYTHONPATH="$NOYAML_DIR" bash -c '
|
|
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
|
get_gitea_token_for_login "$1" "$2"
|
|
' _ "$1" "$2"
|
|
) 2>/dev/null || true
|
|
}
|
|
|
|
# Resolve a token via the normal path (uses PyYAML when installed).
|
|
token_pyyaml() {
|
|
(
|
|
cd "$REPO_DIR"
|
|
XDG_CONFIG_HOME="$FIXTURE_XDG" bash -c '
|
|
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
|
get_gitea_token_for_login "$1" "$2"
|
|
' _ "$1" "$2"
|
|
) 2>/dev/null || true
|
|
}
|
|
|
|
assert_token() {
|
|
local desc="$1" expected="$2" login="$3" host="$4" got
|
|
got=$(token_fallback "$login" "$host")
|
|
if [[ "$got" != "$expected" ]]; then
|
|
echo "FAIL fallback [$desc]: expected [$expected] got [$got]" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$HAVE_PYYAML" == true ]]; then
|
|
got=$(token_pyyaml "$login" "$host")
|
|
if [[ "$got" != "$expected" ]]; then
|
|
echo "FAIL pyyaml [$desc]: expected [$expected] got [$got]" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# 1. Plain, well-formed entry resolves its token.
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_PLAIN
|
|
'
|
|
assert_token "plain scalar" "TOK_PLAIN" primary git.example
|
|
|
|
# 2. A token nested inside a deeper SUB-MAP must NOT attach to the entry — PyYAML
|
|
# resolves the entry's own token to None here, so the fallback must too.
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
extra:
|
|
token: TOK_NESTED_ATTACKER
|
|
- name: other
|
|
url: https://git.example
|
|
token: TOK_OTHER
|
|
'
|
|
assert_token "nested sub-map token is not attributed" "" primary git.example
|
|
assert_token "sibling entry still resolves its own token" "TOK_OTHER" other git.example
|
|
|
|
# 3. A MIS-INDENTED token line (deeper than the entry's fields) must not attach;
|
|
# PyYAML errors on this shape, so both fail closed.
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_MISINDENT
|
|
'
|
|
assert_token "mis-indented token fails closed" "" primary git.example
|
|
|
|
# 4. A trailing inline comment on a scalar is stripped, exactly as PyYAML does.
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_INLINE # trailing note
|
|
'
|
|
assert_token "inline comment stripped" "TOK_INLINE" primary git.example
|
|
|
|
# 5. A PyYAML-fail-closed case: tab indentation. PyYAML raises a scanner error;
|
|
# the fallback resolves no token. Both fail closed identically.
|
|
write_fixture "$(printf 'logins:\n - name: primary\n url: https://git.example\n\ttoken: TOK_TAB\n')"
|
|
assert_token "tab-indent fails closed like PyYAML" "" primary git.example
|
|
|
|
# 6. Host binding is scheme + host + EFFECTIVE PORT, not hostname alone.
|
|
write_fixture 'logins:
|
|
- name: ported
|
|
url: https://git.example:8443
|
|
token: TOK_PORTED
|
|
'
|
|
assert_token "explicit port exact match accepted" "TOK_PORTED" ported git.example:8443
|
|
assert_token "portless repo host rejects :8443 login" "" ported git.example
|
|
assert_token "wrong explicit port rejected" "" ported git.example:9443
|
|
|
|
# 7. An implicit (portless) login URL equals the scheme's explicit default port.
|
|
write_fixture 'logins:
|
|
- name: defported
|
|
url: https://git.example
|
|
token: TOK_DEFPORT
|
|
'
|
|
assert_token "implicit https vs explicit :443 match" "TOK_DEFPORT" defported git.example:443
|
|
assert_token "implicit https vs :8443 rejected" "" defported git.example:8443
|
|
|
|
# 8. An UNQUOTED token whose raw text PyYAML's implicit resolver types as a
|
|
# NON-string (int / null / bool / float) must fail closed: PyYAML yields a
|
|
# non-str value that _accept rejects, so the fallback must NOT surface the
|
|
# stringified scalar as a credential. Each raw form fails closed IDENTICALLY
|
|
# to PyYAML (a prior residual emitted "12345"/"null"/"true"/etc. here).
|
|
assert_nonstring_token_fails_closed() {
|
|
local desc="$1" raw="$2"
|
|
write_fixture "logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: ${raw}
|
|
"
|
|
assert_token "$desc" "" primary git.example
|
|
}
|
|
assert_nonstring_token_fails_closed "unquoted int token fails closed" "12345"
|
|
assert_nonstring_token_fails_closed "unquoted null token fails closed" "null"
|
|
assert_nonstring_token_fails_closed "unquoted tilde-null token fails closed" "~"
|
|
assert_nonstring_token_fails_closed "unquoted yes(bool) token fails closed" "yes"
|
|
assert_nonstring_token_fails_closed "unquoted true(bool) token fails closed" "true"
|
|
assert_nonstring_token_fails_closed "unquoted float token fails closed" "3.14"
|
|
|
|
# 9. A QUOTED scalar is ALWAYS a string, even when its contents look like a
|
|
# non-string implicit form. The quotes force str typing in PyYAML, so the
|
|
# fallback must accept the literal (quote-stripped) contents as the token.
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: "12345"
|
|
'
|
|
assert_token "double-quoted digit token is a literal string" "12345" primary git.example
|
|
write_fixture "logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: 'abc'
|
|
"
|
|
assert_token "single-quoted token is a literal string" "abc" primary git.example
|
|
|
|
# assert_fallback_fails_closed: the forced-fallback path MUST resolve no token
|
|
# (fail closed). Used for STRUCTURAL cases where PyYAML would resolve a DIFFERENT
|
|
# token (e.g. duplicate-key last-wins) — the fallback must never surface the
|
|
# wrong/stale token, so it fails closed instead; when PyYAML is present we also
|
|
# confirm it really does resolve a (divergent) token, proving the fallback is the
|
|
# strictly-more-conservative side and the case is a genuine fail-open guard.
|
|
assert_fallback_fails_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 [[ -z "$got" ]]; then
|
|
echo "FAIL [$desc]: expected PyYAML to resolve a divergent token" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# 10. tea's REAL on-disk shape: the `logins:` block SEQUENCE items sit at the
|
|
# SAME indentation as the key (dash at column 0), with extra scalar fields.
|
|
# The recognizer must resolve this exactly like PyYAML (regression guard so
|
|
# the stricter whole-document recognizer does not fail closed on real input).
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_REAL
|
|
default: false
|
|
ssh_host: ""
|
|
- name: other
|
|
url: https://other.example
|
|
token: TOK_REAL_OTHER
|
|
preferences:
|
|
editor: false
|
|
flags: null
|
|
'
|
|
assert_token "tea dash-at-column-0 real shape resolves" "TOK_REAL" primary git.example
|
|
assert_token "tea real shape sibling resolves own token" "TOK_REAL_OTHER" other other.example
|
|
|
|
# 11. NESTED-SHADOW: a nested `logins:` (NOT at root scope) must not be mistaken
|
|
# for the real root logins. The recognizer parses whole-document structure,
|
|
# so it selects the ROOT logins token exactly as PyYAML does — never the
|
|
# nested attacker token. (A prior line scan matched the FIRST logins at ANY
|
|
# indent and returned ATTACKER.)
|
|
write_fixture 'outer:
|
|
logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: ATTACKER_NESTED
|
|
logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: ROOT_TOK
|
|
'
|
|
assert_token "nested logins shadow selects ROOT token" "ROOT_TOK" primary git.example
|
|
|
|
# 12. BLOCK-SCALAR-SHADOW: text inside a YAML literal/folded block ( | or > ) is
|
|
# an OPAQUE scalar to PyYAML (so `logins` is a string, not a list) and must
|
|
# not be scanned as live logins entries. Both fail closed.
|
|
write_fixture 'logins: |
|
|
- name: primary
|
|
url: https://git.example
|
|
token: ATTACKER_BLOCK
|
|
'
|
|
assert_token "block-scalar logins value fails closed" "" primary git.example
|
|
# A folded/literal block scalar anywhere is outside the recognizer's subset, so
|
|
# the fallback fails closed (conservative) even though PyYAML can still resolve
|
|
# the real root token past the opaque scalar. Fail-closed is the safe side.
|
|
write_fixture 'note: >
|
|
logins:
|
|
- name: primary
|
|
token: ATTACKER_FOLDED
|
|
logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: ROOT_OK
|
|
'
|
|
assert_fallback_fails_closed "folded block scalar present fails closed" primary git.example
|
|
|
|
# 13. DUPLICATE-ROOT / DUPLICATE-FIELD: a duplicated `logins:` root key (PyYAML
|
|
# last-wins) or a duplicated field within a login must fail closed rather
|
|
# than take the FIRST (stale) value. PyYAML resolves the LAST; the fallback
|
|
# refuses to guess.
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: FIRST_DUP
|
|
logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: LAST_DUP
|
|
'
|
|
assert_fallback_fails_closed "duplicate root logins key fails closed" primary git.example
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: FIRST_FIELD
|
|
token: SECOND_FIELD
|
|
'
|
|
assert_fallback_fails_closed "duplicate token field fails closed" primary git.example
|
|
|
|
# 14. MALFORMED-AFTER-VALID: a syntax error LATER in the file makes PyYAML reject
|
|
# the WHOLE document; the recognizer must too (not emit the earlier token).
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_PLAIN
|
|
broken: a: b: c
|
|
'
|
|
assert_token "malformed line after valid login fails closed" "" primary git.example
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_PLAIN
|
|
broken: [unclosed
|
|
'
|
|
assert_token "unclosed flow after valid login fails closed" "" primary git.example
|
|
|
|
# 15. EXTRA-DOCUMENT: a multi-document file (--- separator, or ... end marker)
|
|
# makes PyYAML safe_load reject multi-document input; the recognizer fails
|
|
# closed on ANY document marker rather than emit the first doc's token.
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_PLAIN
|
|
---
|
|
logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: SECOND_DOC
|
|
'
|
|
assert_token "second document (--- separator) fails closed" "" primary git.example
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_PLAIN
|
|
...
|
|
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
|
|
|
|
# 17. TAB / SCANNER PARITY: PyYAML raises a ScannerError on a tab used anywhere
|
|
# outside a quoted scalar -- leading, trailing, or embedded in a plain value,
|
|
# immediately after a key colon, before a key colon, or as indentation -- and
|
|
# yields NO token, accepting tabs ONLY inside single/double-quoted scalars
|
|
# (where the tab is preserved as string content). A prior fallback swallowed
|
|
# those tabs (via .strip()/.rstrip() normalization and [ \t] key separators)
|
|
# and still emitted the login token -- a fail-open in the dangerous direction.
|
|
# The recognizer now fails CLOSED for the whole document on any tab PyYAML
|
|
# rejects, while preserving the tabs PyYAML keeps (inside quotes). All tab
|
|
# positions were verified empirically against PyYAML 6.0.3 (ScannerError for
|
|
# each rejected position; string-preserved for quoted inner tabs).
|
|
TAB=$'\t'
|
|
# Fail-close: a tab in a plain value position (trailing / leading / embedded).
|
|
write_bad_key_fixture "l4o${TAB}"
|
|
assert_both_fail_closed "trailing tab in plain value fails closed" primary git.example
|
|
write_bad_key_fixture "${TAB}9"
|
|
assert_both_fail_closed "leading tab in plain value fails closed" primary git.example
|
|
write_bad_key_fixture "a${TAB}b"
|
|
assert_both_fail_closed "embedded tab in plain value fails closed" primary git.example
|
|
# Fail-close: a tab immediately after the key colon (no separating space).
|
|
write_fixture "bad:${TAB}9
|
|
logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_PLAIN
|
|
"
|
|
assert_both_fail_closed "tab immediately after key colon fails closed" primary git.example
|
|
# Fail-close: a tab used as indentation (before a sequence dash).
|
|
write_fixture "logins:
|
|
${TAB}- name: primary
|
|
url: https://git.example
|
|
token: TOK_PLAIN
|
|
"
|
|
assert_both_fail_closed "tab used as indentation fails closed" primary git.example
|
|
# Fail-close: a tab trailing a sequence-mapping field value.
|
|
write_fixture "logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_PLAIN${TAB}
|
|
"
|
|
assert_both_fail_closed "tab trailing a seq field value fails closed" primary git.example
|
|
# NOT over-broad: a tab strictly INSIDE a quoted scalar is valid YAML (PyYAML
|
|
# keeps it as string content), so the document parses and the login token still
|
|
# resolves in BOTH paths -- double-quoted and single-quoted.
|
|
write_bad_key_fixture "\"a${TAB}b\""
|
|
assert_token "tab inside a double-quoted value still resolves token" "TOK_PLAIN" primary git.example
|
|
write_bad_key_fixture "'a${TAB}b'"
|
|
assert_token "tab inside a single-quoted value still resolves token" "TOK_PLAIN" primary git.example
|
|
# ...and a quoted token value carrying an inner tab resolves to the exact string
|
|
# (tab preserved), identical to PyYAML's construction.
|
|
write_fixture "logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: \"T${TAB}OK\"
|
|
"
|
|
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
|
|
|
|
# 21. PRINTABLE-BOUNDARY FAIL-CLOSE (#865 round-10 blocker 1): the round-9 guard
|
|
# used a hand-rolled C0/DEL subset that MISSED code points PyYAML's Reader
|
|
# also rejects -- the C1 block (U+0080-0084, U+0086-009F) and the BMP
|
|
# noncharacters U+FFFE/U+FFFF -- so the fallback still emitted the token from
|
|
# documents PyYAML rejects whole (fail-open). The guard now uses PyYAML
|
|
# 6.0.3's EXACT Reader.NON_PRINTABLE character class (see detect-platform.sh
|
|
# _FORBIDDEN_CONTROL). Verified empirically against real PyYAML 6.0.3:
|
|
# PRINTABLE = {0x09,0x0A,0x0D, 0x20-0x7E, 0x85(NEL), 0xA0-0xD7FF,
|
|
# 0xE000-0xFFFD, 0x10000-0x10FFFF}; everything else fails the whole document
|
|
# closed. write_codepoint_fixture emits a chosen Unicode code point's real
|
|
# UTF-8 bytes (via python3, since bash strings cannot faithfully carry many
|
|
# of these) into a selectable position, then the login block follows.
|
|
write_codepoint_fixture() {
|
|
# $1 = hex code point (e.g. 0x80); $2 = position: field|comment|token|nelterm
|
|
CP_HEX="$1" CP_POS="$2" python3 - "$FIXTURE_XDG/tea/config.yml" <<'PY'
|
|
import sys
|
|
cp = int(__import__("os").environ["CP_HEX"], 16)
|
|
pos = __import__("os").environ["CP_POS"]
|
|
ch = chr(cp)
|
|
head = "logins:\n - name: primary\n url: https://git.example\n token: TOK_PLAIN\n"
|
|
if pos == "field":
|
|
doc = head + "other: x" + ch + "y\n"
|
|
elif pos == "comment":
|
|
doc = head + "# note x" + ch + "y here\n"
|
|
elif pos == "token":
|
|
doc = "logins:\n - name: primary\n url: https://git.example\n token: T" + ch + "K\n"
|
|
elif pos == "nelterm":
|
|
# NEL (U+0085) used as the line terminator throughout: PyYAML treats it as a
|
|
# line break (printable, NOT a ReaderError) and resolves the token; the
|
|
# fallback's splitlines() splits on NEL identically -> parity, token resolves.
|
|
doc = ("logins:" + ch + " - name: primary" + ch
|
|
+ " url: https://git.example" + ch + " token: TOK_NEL" + ch)
|
|
else:
|
|
raise SystemExit("bad pos")
|
|
with open(sys.argv[1], "w", encoding="utf-8") as f:
|
|
f.write(doc)
|
|
PY
|
|
}
|
|
|
|
# C1-block + BMP-noncharacter code points fail the WHOLE document closed in an
|
|
# unrelated field and in a comment, exactly as PyYAML's ReaderError does.
|
|
for cphex in 0x80 0x81 0x84 0x86 0x9f 0xfffe 0xffff; do
|
|
write_codepoint_fixture "$cphex" field
|
|
assert_both_fail_closed "code point $cphex in unrelated field fails closed" primary git.example
|
|
write_codepoint_fixture "$cphex" comment
|
|
assert_both_fail_closed "code point $cphex in a comment fails closed" primary git.example
|
|
done
|
|
|
|
# NOT over-broad: printable code points PyYAML ACCEPTS must still resolve the
|
|
# token in BOTH paths -- NEL(0x85) as a line separator, U+00A0 (NBSP) inside a
|
|
# value, and an astral code point (U+1F600) inside the token value.
|
|
write_codepoint_fixture 0x85 nelterm
|
|
assert_token "NEL (U+0085) line-terminator resolves token" "TOK_NEL" primary git.example
|
|
write_codepoint_fixture 0xa0 field
|
|
assert_token "U+00A0 in unrelated value still resolves token" "TOK_PLAIN" primary git.example
|
|
write_codepoint_fixture 0x1f600 token
|
|
assert_token "astral U+1F600 inside token resolves verbatim" "$(printf 'T\360\237\230\200K')" primary git.example
|
|
|
|
# 22. INTERNAL-INDICATOR OVER-REJECTION (#865 round-10 blocker 2): the round-9
|
|
# recognizer blanket-rejected any plain scalar CONTAINING a flow indicator
|
|
# ([]{}*&!), but in BLOCK context PyYAML treats ',[]{}' as ordinary content
|
|
# and treats '!&*#...' as significant ONLY at the FIRST non-space char. So an
|
|
# INTERNAL indicator is legal plain-string content and PyYAML emits the token
|
|
# verbatim; the fallback dropped it (over-rejection). Verified empirically
|
|
# against real PyYAML 6.0.3. The fix removes the blanket internal scan while
|
|
# the leading-char guard and the ' #'/': '/trailing-':' guards keep the
|
|
# fail-OPEN direction shut.
|
|
for tv in 'a!b' 'a,b' 'a[b' 'a]b' 'a{b' 'a}b' 'a&b' 'a*b' 'a[b]c' 'a{b}c' 'a,b,c' 'a#b' 'a:b'; do
|
|
write_fixture "logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: ${tv}
|
|
"
|
|
assert_token "internal-indicator token '$tv' resolves verbatim" "$tv" primary git.example
|
|
done
|
|
|
|
# Fail-OPEN direction stays shut: a LEADING indicator, a ' #' comment tail, an
|
|
# internal ': ' (colon-space) inline map, and a trailing ':' each make PyYAML
|
|
# resolve NO usable string token (tag/flow/anchor reject or None, comment strip,
|
|
# or a mapping), so BOTH must fail closed. (Leading tag/anchor/flow are the
|
|
# documented, round-9-approved structural fail-closed class; kept intact here.)
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: !x
|
|
'
|
|
assert_both_fail_closed "leading '!' tag token fails closed" primary git.example
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: [a]
|
|
'
|
|
assert_both_fail_closed "leading '[' flow-seq token fails closed" primary git.example
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: a # trailing comment
|
|
'
|
|
# ' #' comment tail: PyYAML strips the comment -> token is the string 'a', which
|
|
# still resolves. This is the NOT-over-broad boundary partner of the guard.
|
|
assert_token "space-hash comment tail strips to plain token" "a" primary git.example
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: a: b
|
|
'
|
|
assert_both_fail_closed "internal colon-space (inline map) token fails closed" primary git.example
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: ab:
|
|
'
|
|
assert_both_fail_closed "trailing colon (map indicator) token fails closed" primary git.example
|
|
|
|
echo "Gitea login resolution regression harness passed"
|