Skip to content

release: 0.6.0 — fix dash-prefixed credential leak, *.* allowed_hosts bypass, and publish-path digest verification - #49

Merged
blackaxgit merged 2 commits into
mainfrom
ci/fix-digest-verification
Jul 26, 2026
Merged

release: 0.6.0 — fix dash-prefixed credential leak, *.* allowed_hosts bypass, and publish-path digest verification#49
blackaxgit merged 2 commits into
mainfrom
ci/fix-digest-verification

Conversation

@blackaxgit

@blackaxgit blackaxgit commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Prepares the 0.6.0 release. Three defects, all confirmed by execution and all pre-existing on main.

1. Credential leak: dash-prefixed values (security)

_redact_secrets left a space-separated credential value unredacted when it began with -, so it reached logs and OTel spans in plaintext. Generated passwords routinely start with -, so this is not a corner case.

BEFORE                                            AFTER
mysql --password -Xy9$kL2mQp db   (leaked)   ->   mysql --password {REDACTED} db
mysql --password -8Hn!vB3wZr db   (leaked)   ->   mysql --password {REDACTED} db

The guard that treats a following dash token as "this flag was boolean" existed to fix an earlier real leak (a credential flag following a boolean flag being silently skipped). It is narrowed from - to -- rather than removed, so that fix stays intact — --password --token=x still redacts --token=x.

Documented residual: a value beginning with -- is still treated as a flag and not redacted. Widening further would reopen the skip-leak. Redaction is a tripwire; the documented mitigation remains env files or stdin over argv.

2. SSH_MCP_HTTP_ALLOWED_HOSTS=*.* bypassed DNS-rebinding protection (security)

*.* was listed in the refusal set but the suffix-wildcard allowance (*.internal.example.com) was evaluated first, so it slipped through. *.* matches effectively any dotted hostname — on the HTTP transport that is remote command execution behind a rebinding attack.

Validation now strips only the two deliberately-permitted wildcard forms and demands a concrete remainder. Full truth table verified empirically:

Refused Permitted
*, *:*, *.*, *.*:*, *.*.*, *.:*, a*b.example.com, .., whitespace-only *.internal.example.com, *.internal.example.com:*, ok.example.com:*, ok.example.com, *.com

Side effects, both fail-closed: infix wildcards like a*b.example.com were never caught before, and a whitespace-only value previously fell back silently to loopback-only. *.com stays permitted, consistent with the documented suffix-wildcard policy.

3. Publish-path digest verification (CI + supply chain)

Docker Build & Push failed on every push to main. docker buildx imagetools create does not reuse the source digest — it wraps the scanned manifest in a new OCI index — so the check compared an index against one of its children.

:latest -> index   sha256:8204263e…
             child sha256:12db37ce…   <- the scanned digest

The Trivy gate was never bypassed: the scan runs against the digest before any tag is promoted, so published content was always scanned content. But the broken check sits before Attest build provenance, so images published to GHCR between 2026-07-26 and this release carry no provenance attestation. Verification now asserts the invariant that matters — the set of image manifests a tag serves is exactly the scanned digest — inspecting the index's children and ignoring attestation manifests.

Also

  • except asyncio.TimeoutError sat after except OSError; since asyncio.TimeoutError is TimeoutError and TimeoutError subclasses OSError on Python ≥3.11, it was unreachable dead code and every timeout logged the less specific message. Reordered.
  • test_fuzzed_long_flag_junk_prefix_is_unbounded now asserts exact output. It was a latent test defect: a generated secret that is a substring of the flag name (credenti inside credential) reported a false "leak". It passes on a fresh checkout but fails deterministically once Hypothesis caches the counterexample — a landmine for release CI. Confirmed pre-existing by reverting both source files.
  • Version bumped to 0.6.0; CHANGELOG [Unreleased] closed as [0.6.0] - 2026-07-26, and the now-inaccurate claim that tag verification "fails the job if any tag resolves elsewhere" corrected.

Verification

  • pytest717 passed
  • ruff format --check / ruff check src/ tests/ / mypy src/ / bandit -r src/ -q / pip-audit — all clean
  • gitleaks scoped to origin/main..HEAD — no leaks
  • Every new regression test verified to fail on the unfixed code (by stashing each source file) and pass after
  • Both security fixes independently re-verified by me against realistic inputs, not just via the agents' reports

Operator action still required before tagging

  1. Configure PyPI Trusted Publishing for release.yml plus the reviewer-gated pypi environment, and delete any legacy API token. Cannot be done from repo files.
  2. Note that a v0.6.0 tag publishes twice: release.yml to PyPI (reviewer-gated) and ci.yml's docker job to GHCR (ungated).

…tation

`docker buildx imagetools create` wraps the scanned manifest in a new OCI
index rather than reusing its digest, so comparing the tag's own digest
against steps.build.outputs.digest compared an index to one of its children
and failed on every push to main. Because the check sits before the
attestation step, images published since 2026-07-26 carry no provenance.

- Verify the set of image manifests a tag serves is exactly {scanned digest},
  ignoring attestation manifests, instead of hashing raw manifest bytes.
- Confirmed against the live registry: :latest was index sha256:8204263e
  whose sole child was the scanned sha256:12db37ce.
- Regression test fails on the previous workflow and passes on this one.

The Trivy gate was never bypassed: the scan runs against the digest before
any tag is promoted.
…osts, cut 0.6.0

Two credential/network security defects found while preparing the release,
both pre-existing on main:

- _redact_secrets left a space-separated credential value unredacted when it
  began with "-" (mysql --password -Xy9$kL2mQp leaked verbatim). The guard
  treating a following dash token as "this flag was boolean" is narrowed from
  "-" to "--", which keeps the earlier skip-leak fix intact. A value beginning
  with "--" remains a documented residual.
- SSH_MCP_HTTP_ALLOWED_HOSTS=*.* was accepted despite being in the refusal
  set: the suffix-wildcard allowance was evaluated first. Validation now
  strips only the two permitted wildcard forms and demands a concrete
  remainder, which also refuses infix wildcards and whitespace-only values.

Also: reorder except asyncio.TimeoutError before except OSError, where it was
unreachable dead code on Python >=3.11, restoring the specific timeout
message; make test_fuzzed_long_flag_junk_prefix_is_unbounded assert exact
output so a secret that is a substring of the flag name no longer reports a
false leak; bump to 0.6.0 and close the CHANGELOG entry.
@blackaxgit blackaxgit changed the title ci: fix publish-path digest verification and restore provenance attestation release: 0.6.0 — fix dash-prefixed credential leak, *.* allowed_hosts bypass, and publish-path digest verification Jul 26, 2026
@blackaxgit
blackaxgit merged commit 12fd2a3 into main Jul 26, 2026
7 checks passed
@blackaxgit
blackaxgit deleted the ci/fix-digest-verification branch July 26, 2026 05:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant