Skip to content

Audit round two: four more ways the tool could be turned on its user - #35

Merged
Tippel-AI merged 1 commit into
mainfrom
fix/audit-round2
Sep 1, 2026
Merged

Audit round two: four more ways the tool could be turned on its user#35
Tippel-AI merged 1 commit into
mainfrom
fix/audit-round2

Conversation

@Tippel-AI

Copy link
Copy Markdown
Owner

Follows #34. The remaining security-shaped findings from the launch audit.

keygen printed a permission it had not set

  private key  /path/markproof-signing-key.pem  (mode 600)

…over a file that was -rw-r--r--. os.open's mode argument applies only on
creation
, so a key file left by an earlier run under a wider umask, or a
placeholder somebody touched, keeps its permissions through O_TRUNC.

I checked this one before believing it — a fresh keygen really does produce 600,
and the auditor's claim looked wrong until I tried the pre-existing-file case.
fchmod on the open descriptor closes it with no window in which the path could
be swapped.

A claim printed next to a fact that contradicts it is precisely the defect class
this project exists to find in other people's systems.

The response body chose what markproof fetched

The media probe fetches asset URLs out of the endpoint's own JSON. So the endpoint
under test decided what a trusted process requested from inside the operator's
network — and http://169.254.169.254/latest/meta-data/ answers with instance
credentials on every major cloud.

Loopback, private, link-local, reserved, multicast and unspecified addresses are
refused when the probe's own target is not itself internal. That condition is
the interesting part: pointing markproof at the demo bot on 127.0.0.1 is the
documented way to try it, and an absolute rule would have broken the first thing
the README asks a newcomer to do.

Literal addresses only — resolving hostnames would make a verdict depend on DNS at
check time, which is what the determinism claim rules out. That defence belongs in
the network the runner sits on.

Two smaller ones

patterns_file / labels_file could leave the packaged directory. A rulepack
comes from a path the operator passes, so it is only as trusted as that file, and
the loader resolved these against the package directory without checking they were
names. Now validated as bare filenames.

The manifest fetch was unbounded. The document had a size limit; advertising a
manifest was the cheapest way around it. Non-200 is now refused rather than only
4xx/5xx — a 204 carries no manifest either, and handing the checker an empty body
while calling it a manifest is the wrong kind of wrong.

Verification

Each fix has a test that fails without it, including the parameterised SSRF cases
(169.254.169.254, 127.0.0.1, 10.0.0.5, [::1]) and the "a local target may
still serve local assets" case that keeps the demo working.

522 tests, ruff + mypy strict clean.

Docs and README findings follow.

🤖 Generated with Claude Code

**`keygen` printed "(mode 600)" over a world-readable key.** `os.open`'s mode
argument applies only when the file is created, so a key file left by an earlier
run under a wider umask — or a placeholder somebody touched — kept its
permissions through `O_TRUNC` while the CLI reported 600. A claim printed next to
a fact that contradicts it is the defect class this project exists to find
elsewhere. `fchmod` on the open descriptor closes it without a window in which the
path could be swapped.

**The media probe fetched any URL the response body named.** The endpoint under
test supplies asset URLs, so it chose what markproof requested — and pointed at
169.254.169.254, which on every major cloud answers with instance credentials,
that is a server-side request forgery made by a trusted process from inside the
operator's network. Loopback, private, link-local and reserved ranges are now
refused, but only when the probe's own target is not itself internal: pointing
markproof at a bot on localhost is the documented way to try it, and breaking that
would break the demo the README sends people to.

Literal addresses only. Resolving hostnames here would make a verdict depend on
DNS at check time, which is exactly what the determinism claim rules out.

**`patterns_file` and `labels_file` could leave the packaged directory.** A
rulepack is loaded from a path the operator passes, so its contents are only as
trusted as that file — and the loader resolved these names against the package
directory without checking they were names. Both are now validated as bare
filenames.

**The manifest fetch had no size limit and loose status handling.** The document
had a limit; advertising a manifest was the cheapest way around it. Non-200 is now
refused rather than only 4xx and 5xx: a 204 carries no manifest either, and
handing the checker an empty body while calling it a manifest is the wrong kind of
wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Tippel-AI
Tippel-AI merged commit e2c49d2 into main Sep 1, 2026
8 checks passed
@Tippel-AI
Tippel-AI deleted the fix/audit-round2 branch September 1, 2026 20:35
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