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
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:
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user