Skip to content
This repository was archived by the owner on Oct 10, 2026. It is now read-only.

v0.6.0 - the sweep closes

Choose a tag to compare

@Dashtid Dashtid released this 01 Sep 23:41
· 33 commits to main since this release
7ade69f

This release closes the last of the audit-sweep leads. Two findings: a defect in
the evaluator that decides whether a GCP vector passes, and a tranche covering a
GitHub encoding rule whose own documented example turns out to be an org-wide pin.

The CEL string decoder lost a character

cel.py decoded string literals with three sequential str.replace passes.
Checked against the CEL spec's own ESCAPE production, that is wrong three ways —
and the first is a wrong answer, not a limitation.

A double quote needs no escape inside a single-quoted string, so a literal holding
backslash-backslash-quote is legal and means backslash-quote. Replacing \\ → \
first and \" → " second made the second pass consume the backslash the first
had just written, silently returning a bare quote. In the oracle that decides
whether a vector passes, a lost character can make a vector pass for the wrong
reason.

Two more: ten escape forms were passed through as backslash-plus-character
(\n \t \r \a \b \f \v \?, backtick, \xHH, \uHHHH, \UHHHHHHHH, octal), and
an invalid escape raised nothing, so this evaluator would happily evaluate an
expression GCP itself refuses to save.

Fixed by scanning the literal once, left to right — the structural point being that
a single pass cannot re-read its own output, so no reordering of the replace chain
could have been correct. Raw (r-prefixed) literals are now supported, which is the
idiomatic way to write a regex inside matches(). Triple-quoted and bytes literals
get a refusal that names them.

No verdict moved. No shipped vector contains a backslash in a CEL expression.
Six of the seven new tests were confirmed to fail against the pre-fix decoder.

GitHub's %3A rule, and the example that ships an org-wide pin

GitHub's OIDC reference: "Any : within the metadata values will be replaced with
%3A in the subject claim."
An environment named Production:V1 mints
repo:octo-org/octo-repo:environment:Production%3AV1.

Four vectors, because the interesting part is the chain, not the rule:

  • The literal pin fails closed. Pinning the name as read off the GitHub UI
    produces five colons against a subject with four. Nothing matches, every
    deployment is refused, and the failure gives no hint of its cause — policy and
    environment name look identical to a reader.
  • The obvious repair opens the door. environment:Production* admits the
    intended environment and every other one with that prefix. Protection rules are
    configured per environment, so the compensating control the original pin leaned
    on does not extend to whatever the wildcard picked up. The encoding surprise
    causes the over-permission.
  • GitHub's own second example omits the repository. Printed as a condition
    value to copy: "sub": "environment:production%3Aeastus:repository_owner:octo-org".
    The encoding is correct and the pin never names the repository, so every repo in
    the org with an environment of that name satisfies it. Wildcard-free, so a scanner
    grading conditions by looking for * sees an exact pin and flags nothing.

New vocabulary tag percent-encoded.

Not shipped, recorded as a probe. The documented rule is a single-character
substitution, not percent-encoding — nothing says % is itself escaped. If it is
not, an environment named Production%3AV1 mints the same subject as one named
Production:V1, and a pin on either admits the other. GitHub documents no character
allowlist for environment names, so that is an inference, and this corpus does not
ship inferences.

One grade that stated its assumption as a fact

gh-aws-environment-subject-exact asserted the compensating control outright while
its sibling states the same thing as an assumption. The judgment catalog's rule for
the family is explicit — safe when protection rules exist, caution otherwise — so
the grade stays safe and the reason now names what it rests on.

Corpus

14 suites / 151 vectors; github-aws 0.6.0. Provenance unchanged at
11 observed. 389 tests.

Install

pip install subvectors==0.6.0

The wheel ships the entire corpus. The vectors are CC0; everything else is
Apache-2.0.