fix(git): fail closed on YAML anchors in tea token fallback (#865 round 12)
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful

The PyYAML-absent fallback's _strip_properties consumed a plain anchor
('&name') per-scalar, with no document-scoped registry of declared anchor
names. Real PyYAML's Composer rejects a duplicate anchor NAME declared
anywhere in the document (ComposerError, whole-document fail), but the
fallback would still emit the later token: fail-open (HIGH). Fix: reject
every '&'-anchor property unconditionally instead of building a hand-rolled
duplicate registry, which guarantees zero fail-open by construction at the
cost of a deliberate, endorsed over-reject on a single non-duplicated
anchor. The '!' non-specific-tag branch is unchanged ('! x' -> 'x' still
resolves).
This commit is contained in:
Hermes Agent
2026-07-22 03:07:56 -05:00
parent 2d821324fe
commit 8ac7e70f0d
2 changed files with 124 additions and 39 deletions

View File

@@ -843,9 +843,6 @@ _FAIL = object()
# position makes the whole line fail to match -> the caller fails closed.
_KEY_RE = re.compile(r"^([A-Za-z0-9_][A-Za-z0-9_.\-]*) *:(?:[ ](.*)|)$")
_FLOW = set("[]{}*&!")
# A plain anchor property: '&' then PyYAML's exact anchor charset [0-9A-Za-z-_]+,
# then whatever follows (captured for the caller to require a space or EOL).
_ANCHOR_RE = re.compile(r"^&[0-9A-Za-z\-_]+(.*)$", re.S)
class _Bail(Exception):
@@ -856,22 +853,32 @@ class _Bail(Exception):
def _strip_properties(value):
# Consume leading YAML node properties -- at most ONE non-specific tag
# ('!' + space) and at most ONE plain anchor ('&name' + space), in either
# order -- and return the remaining node text for normal resolution. PyYAML's
# SafeLoader applies these TRANSPARENT properties and then resolves the
# following node with its ordinary implicit typing, so (verified against real
# PyYAML 6.0.3) '! x' -> 'x', '&a 123' -> int 123, '! ' / '&a' -> null, and
# '! &a x' / '&a ! x' -> 'x', exactly as the bare node would resolve. Returns
# _FAIL for every form we do NOT reproduce identically, so the caller fails
# closed: a tag shorthand ('!x' -> ConstructorError), an explicit or verbatim
# tag ('!!str' / '!!int' / '!<...>' / '!foo') whose type coercion we do not
# emulate, a tab separator (PyYAML scanner error), a duplicate tag or anchor
# ('! ! x' / '&a &b x' -> ParserError), or an anchor glued to a non-space.
# The returned remainder (possibly '') is re-resolved by the caller; '' is a
# null node -> fail closed for a token field, matching PyYAML.
# Consume a leading YAML node non-specific tag ('!' + space) and return the
# remaining node text for normal resolution. PyYAML's SafeLoader applies
# this TRANSPARENT property and then resolves the following node with its
# ordinary implicit typing, so (verified against real PyYAML 6.0.3)
# '! x' -> 'x', '! ' -> null, exactly as the bare node would resolve.
# Returns _FAIL for every form we do NOT reproduce identically, so the
# caller fails closed: a tag shorthand ('!x' -> ConstructorError), an
# explicit or verbatim tag ('!!str' / '!!int' / '!<...>' / '!foo') whose
# type coercion we do not emulate, a tab separator (PyYAML scanner error),
# a duplicate tag ('! ! x' -> ParserError).
#
# ANCHORS ('&name'): deliberately rejected in FULL, not just duplicates.
# Real PyYAML tracks anchor NAMES in a document-scoped composer registry
# and raises ComposerError ("found duplicate anchor") the instant the
# SAME anchor name is declared on a SECOND node anywhere in the document
# -- including on a totally unrelated node far from the token field. This
# line-oriented fallback has no such document-wide registry (and building
# one reliably, across every node shape this recognizer does not even
# parse, risks missing a scope and re-opening the fail-open). So instead
# of emulating the registry, every '&'-anchor property fails closed here,
# unconditionally. This is a conservative OVER-reject relative to PyYAML:
# a single, non-duplicated '&a x' is valid YAML that real PyYAML resolves
# to 'x', but we refuse it too. That is intentional and safe -- fail
# closed can only cost an emitted token PyYAML would have allowed, never
# emit one PyYAML would reject.
seen_tag = False
seen_anchor = False
while value[:1] in ("!", "&"):
if value[0] == "!":
# Non-specific tag: '!' then AT LEAST ONE SPACE. A tab is a PyYAML
@@ -880,18 +887,8 @@ def _strip_properties(value):
return _FAIL
seen_tag = True
value = value[1:].lstrip(" ")
else: # anchor '&name'
m = _ANCHOR_RE.match(value)
if seen_anchor or m is None:
return _FAIL
rest = m.group(1)
if rest == "":
value = "" # bare '&name' with no following node -> null node
elif rest[0] == " ":
value = rest.lstrip(" ")
else:
return _FAIL # '&name' glued to a non-space (e.g. '&a:') -> error
seen_anchor = True
else: # anchor '&name' -- always fail closed, see comment above
return _FAIL
return value