Skip to content

Audit 0.20.0 on 103 projects: crash and hang fixes, security checks, precision caps - #26

Merged
tauanbinato merged 39 commits into
mainfrom
audit-0.21
Sep 26, 2026
Merged

tauanbinato merged 39 commits into
mainfrom
audit-0.21

Conversation

@tauanbinato

@tauanbinato tauanbinato commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

An audit of 0.20.0 against 103 pinned projects: the existing corpus, 24 open-source projects of kinds not tried before (intentionally vulnerable Rails, Node, GraphQL, C# and Java apps, a Deno framework, a WordPress plugin, a cookiecutter template, Angular, a Rocket server, Go web apps, and projects in Kotlin, Swift, Elixir and C), and 8 more of the maintainer's own. Findings were labeled by hand against the code.

Results

0.20.0 this branch
Reviews right, 70 labeled projects 69% (192 wrong) 75% (136 wrong)
Considers right, 70 labeled projects 65% (354 wrong) 72% (254 wrong)
Reviews right, 11 held-out projects 57% 61%
Considers right, 11 held-out projects 54% 56%
Undecided units (of judged) 2.24% 1.50%
Reviews right, 14 fresh projects (never used for tuning) 53% (63 wrong) 59% (45 wrong)
Considers right, 14 fresh projects 47% (225 wrong) 57% (141 wrong)
Undecided units, fresh projects 1.51% 0.87%
Canary problems found 64 of 66 64 of 66
Peak memory, laravel/framework dry run, all rules 4.1 GB 2.2 GB

The fresh projects are 9 more of the maintainer's own repos and 5 open-source kinds not tried before: Online Boutique (polyglot gRPC microservices), refined-github (a browser extension), a React Native template, Uniswap v2 (Solidity) and a dbt project. Both versions' findings were labeled by hand after the other changes here were made. Only the go.mod fix and the two instruction-file fixes came from them (Online Boutique, refined-github). Measured with #27's head, which contains this branch.

No regression on the 0.20.0 corpus run: before these changes, 0.20.0 reproduced the previous audit's run exactly on all 71 projects. JevGate's full self-check with this branch's binary leaves only the baselined visit consider.

Fixes

  • Crashes: two panics on multibyte text (SQL splitting in pgweb, a Python block ending in a CJK comment in vnpy) aborted whole runs with exit 101.
  • Incomplete runs: a Markdown file with NUL bytes made a run incomplete (exit 2); it is now skipped like a binary source file.
  • Hang: jellyfin's dry run ran past half an hour, and now takes 17 s (laravel/framework: 254 s to 88 s). Which files import which is worked out once per file. Requests are byte-identical on all 95 projects compared.
  • Instruction file names: on macOS and Windows, a lowercase agents.md or claude.md was probed as AGENTS.md/CLAUDE.md, failed to read, and made the whole run incomplete (exit 2): refined-github and a React Native template, both new to the corpus. Names now match exactly, and a CLAUDE.md that links to such a file loads it as Claude Code's instructions.
  • Go modules: go.mod is read as a package manifest, so copies between Go modules that don't depend on each other are not compared. Online Boutique's services are separate modules, and 9 of the 10 copies found between them were wrong.
  • Memory: follow-up requests (traces, rechecks, settles) are kept as JSON text until asked. laravel/framework's dry run peaked at 3.2 GB for the security rules and now peaks at 1.5 GB. A corpus run paid for no request, so every request is unchanged.

Findings

  • Access control: a SECURITY DEFINER function nothing revokes EXECUTE on is callable by every role, and the question now covers stored files. chatbot-ui's delete_storage_object (anyone can delete any file) becomes a review. Rows owners mark shared are acceptable.
  • Unsafe settings, new checks: token, key, escaping and plain-text password checks for every language, settled by Choices that name what the code does. A password or token finding stays a review only when the function itself shows the weakness; otherwise it is a consider.
  • Sensitive data: the logging Choice tells audit lines and CLI output from personal data (17 wrong logging reviews gone). Where error text goes is asked for reviews too.
  • Tests: an "asserts internal details" consider is asked, with the code the test calls, what its assertions read. Results, state and effects clear it; stored input and the program's own calls keep it. Labeled tests/value considers went from 29% to 77% right, and held-out from 13 wrong to 1, for $0.006. A test that reads members through reflection or as any keeps its consider.
  • Large docs: a split finding is asked what kind of document it is, now including a plan for one change and requirements. 14 considers on single-subject documents are gone: dated release plans, READMEs, specs.
  • File organization: the same kind of code written out per feature (a mailer's function per template) counts as one job. Every such consider was wrong, and undecided outlines went from 106 to 45.
  • Comments: a comment still undecided once its kind is asked leans on the kind (a note or clear). 1,153 step-heading comments no longer leave their files uncertain.
  • Framework notes: GraphQL resolvers in graphene, strawberry and ariadne; desktop, game and terminal client applications.
  • Level caps:
    • Hardcoded values that only need a name are at most a consider, and a note when their file writes them once.
    • Instruction sections under 15 tokens are notes.
    • Security findings at test paths are one level lower.
    • Copies in retired directories are not compared.
  • Coverage:
    • Deno.test cases.
    • Claude Code skills, commands and subagents as project docs.
    • Cookiecutter/copier template files parsed without their Jinja tags: 31 skipped files of cookiecutter-django are judged.
  • Self-review fixes: the considers JevGate's own review raised on this branch are fixed. The level caps are one function, the new tests share a helper, the settle Choices have their own module, and matching_windows walks seed pairs from an iterator. No finding changes on the corpus.

For your review

Policy calls:

  • Unnamed values at most a consider: before this, 17 such reviews were right and 18 wrong.
  • "Shown in the function" rule for token and password reviews: dvcsharp-api's JWT read without validation, a real flaw, becomes a consider.
  • Leaning comments: a comment left between thresholds after three asks becomes a note or clear, not uncertain.

CI's review job runs the released 0.20.0, which still raises two "same kind of code for several features" considers (settle.rs, test_rules.rs) that this branch's file-organization change removes. The branch's own self-check leaves only the baselined visit.

The changelog has every change with its numbers. Measured-but-rejected: a rewording of the unnamed-value question (see evaluation/experiments, local).

A byte step left the splitter inside a multibyte character, and the next
slice panicked: pgweb's booktown.sql, which holds U+FFFD outside quotes,
aborted the whole check with exit 101.
A Python block keeps its trailing comment, and a block's last byte fell
inside the `线` closing one in vnpy's scripts: slicing the source there
panicked and aborted the check. The SQL splitter's own line helper had
the same slice for a file ending on such a character.
Source files that are binary or not UTF-8 were skipped, but a document
was an error: one Markdown file holding NUL bytes in dvja's docs made
the whole check incomplete (exit 2).
Every security unit looked up its callers and the errors its callees
raise by testing every selected file's imports again, and a C# file's
imports are all its lines of code: jellyfin's dry run ran past half an
hour and now takes 17 seconds, and laravel/framework's took 254 seconds
and now takes 88. Links keeps each file's importers and the files it
reaches as first asked, and a name made of segment characters is looked
up among a file's segments instead of searched for in every line. The
requests are unchanged on all 95 corpus projects.
…the owner's choice

PostgreSQL lets every role execute a new function, but an empty list of
grants and revokes said nothing: chatbot-ui's delete_storage_object,
which deletes any stored file with the service role key and which
nothing revokes, stayed at 0.68 on skipping the caller check and was
only a search_path consider. A policy that lets others read rows their
owners marked shared or public (`sharing <> 'private'`) is the read
side of sharing; ten such policies were considers.
oak writes its 266 tests as Deno.test({ name, fn() {…} }), and neither
the test spans nor the test map knew the call: its test files each got a
file-purpose request and the test rules had nothing to judge. The string
and named-function forms, and .only and .ignore, are cases too.
…ain-text passwords

Code outside C# and Django is asked the token and key checks C# asks: a
JWT decoded without its signature scored 0.93 to 0.98 as a weakened
setting in DVGA and JavaVulnerableLab, and with no check to name it was
only a note; DVNA's session secret 'keyboard cat' and RailsGoat's
encryption key were missed. Every language is asked whether code turns
off HTML escaping (NodeGoat's autoescape: false, RailsGoat's
escape_html_entities_in_json = false). The password check asks about
passwords saved or checked as plain text, which NodeGoat and
JavaVulnerableLab do and no check named. A cookie added to a request or
set empty and expired to delete it is not a session cookie without
flags: shiori's three such cookie reviews were wrong. WordPress's
wp_rand and wp_generate_password are cryptographic.
… the code does

Asked whenever their check is not clear, each can only clear it: what
code does with tokens (front-end hooks that send their own token and
middleware that looks a session up stayed between 0.2 and 0.5 on the new
token check), how it handles users' passwords (HMAC signing was a
password-hash review), and what its logs write, now with who did what
and output shown on purpose to a command-line tool's operator apart from
other personal data: 10 of 19 labeled logging reviews were audit lines
naming who signed in or commands printing recovery codes for the admin
who ran them. A Choice asked whenever its check is not clear also clears
a presence answer or check that found a concern it rules out.
Considers that rest only on whether a value needs a descriptive name
were right 52% of the time on 35 hand-labeled projects: 19 of 52 when
the file writes the value once, 34 of 49 when it repeats it. A delay
given to setTimeout, a size given to an attribute or a CSS class reads
where it is used; a value written twice can drift apart. Such a consider
is now a note that says why, and reviews stay. On the corpus, 156
considers became notes and labeled hardcoded-value considers went from
51% to 65% right. The count is made at planning and asks nothing.
… only rows

chatbot-ui's delete_storage_object deletes any stored object through the
storage API with the service role key and nothing revokes EXECUTE on
it, so anyone can delete any file. Asked about rows only, it stayed at
0.75 on skipping the caller check; asked about rows or stored files, it
and delete_storage_object_from_bucket are reviews at 0.95, and no other
access-control finding changed on six Supabase projects.
…s client input

A graphene resolver's arguments read as parameters any caller could
pass: DVGA's SQL injection in resolve_pastes and SSRF in
ImportPaste::mutate were considers "if a caller passes outside input",
and its command injection in resolve_system_debug a note. A Python file
that imports graphene, strawberry or ariadne now carries a framework
note, as Next.js and SvelteKit files do, and all three are reviews.
Only such files' requests change.
…ms from trusting it

A file at a test path that holds no tests is judged as application code,
and the dummy apps of devise and clearance and a test model hashing with
password.reverse were three wrong reviews, the only security reviews or
considers at test paths across 103 projects. Such findings are one level
lower, like code that runs only in development.

The token Choice separates decoding a token to read what it says (an
expiry, a user or character id) from deciding access with it: code that
read the expiry of a token its identity provider had just sent, or the
character id of its own access token, was chosen as trusting a token
unverified, while every labeled true token finding decided access with
it or turned verification off.
…eviews too, and keep only turned-off token checks at review

An instruction section of fewer than 15 tokens is a note: labeled by
hand, 1 of 10 findings on such sections was right, most of them a title
and a Last updated line read as a record of past work (about 35 of one
repository's considers), against 64 of 68 on larger sections.

Where a function's text goes is asked whenever its error-detail signals
are not clear, and can clear a check that found a concern: a game client
handing the server's error text to its own window over a channel whose
messages are named Response was fifteen reviews for sending details to
a remote client.

A review that only the token check names is a consider unless the code
turns a library's verification off: whether a token was verified before
the function reads it lies outside the function, and such reviews were
right in intentionally vulnerable apps and wrong in three others.
Naming a value is a cleanup, as comments and instruction sections are.
Labeled by hand, 17 reviews that rested only on whether a value needs a
descriptive name were right and 18 wrong, most of the wrong ones tuning
in game, audio and animation code: a scheduler's 500 ms, a hash seed, a
mix gain, a float epsilon. Such findings are at most a consider, and
still a note when their file writes the value once. Findings about a
value that differs between deployments or special-cases one identity
keep their level.
…hoice

A game client hands the server's error text to its own window over a
channel of Response messages; asked where the text goes, it was answered
as a response to a client, and the fifteen reviews for sending internal
details to a remote client stayed. The local option names the screens a
desktop, game or mobile app reaches through a channel, event or IPC
call, and the client option a connected remote client.
A directory named deprecated, archive, attic, retired, obsolete or
proof of concept marks its code as a function marked deprecated does: it
is not worth sharing code with. A Unity project's Assets/ProofOfConcept
builders, kept as a reference with no menu entry, were paired with the
live scene builders in six wrong reviews. legacy is left out, since
legacy code is often still served. No other corpus project has such a
directory.
A password check or token check at review needs its Choice to name what
the function does: hashing with a fast hash, or turning a library's
verification off. Whether a callee or an entity's @BeforeInsert hook
hashes the password a function saves, or whether a token was verified
before the function reads it, lies outside the function: dvja's
register and edit, which pass the password to a service that hashes
it, and nest-realworld's create, whose entity hook hashes it with
argon2, were plain-text reviews. Such findings are considers now, as
unverified-token reviews already were.
A game client hands the server's error text to its own window over a
channel of Response messages, and asked where the text goes the answer
was a remote client, in 16 wrong reviews for sending internal details.
A file whose package depends on an interface toolkit (ratatui, egui,
iced, bevy, tauri, electron, spacetimedb-sdk and others) now carries a
framework note saying the program runs on its user's machine, as
Next.js and SvelteKit files do; all 16 are gone. Only such packages'
requests change: four of the corpus's 103 projects, about $0.20.
Markdown under .claude/skills, .claude/commands and .claude/agents was
not read at all: one project's skill cited documentation paths that a
rename had removed. A session loads only their descriptions and reads
the rest when one is used, so they are project documentation, checked
for stale paths, repetition and size, and do not count toward what
loads at a session's start. A path holding a $ placeholder, such as a
command's .kiro/specs/$1/spec.json, names no file.
A file under a directory whose name holds a {{ … }} placeholder, as a
cookiecutter template's {{cookiecutter.project_slug}} does, held Jinja
statements and placeholders that are no syntax of its language: 31 of
cookiecutter-django's Python and JavaScript files, the generated
application's settings, models, views and tests, were skipped for
syntax errors. Such files are parsed with their statements and comments
blanked and each placeholder read as a name of the same length, so byte
offsets and lines stay the file's and the evidence keeps the tags. All
31 are judged; the Celery settings that turn off Redis certificate
checks are a review. No other corpus project has such a directory.
… test setup

JevGate's review of this branch found Tally::add's chain of caps hiding
its main path and the new unsafe-settings tests repeating their setup.
The caps are one function with early returns, and the tests run through
one helper. No finding changes on the corpus's 103 projects.
The questions file held the presence questions, the checks and the
settle Choices, which JevGate's review found could be read apart. The
Choices move to questions/settle.rs with their text unchanged; no
request or finding changes on the corpus.
…atching_windows

When an outline's split Score stays undecided, the kind of file decides.
The same kind of code written out per feature counted as serving several
features and raised a consider; every such consider was wrong: vaultwarden's
mailer, a game's admin reducers per kind of map content, and the settle
questions JevGate's self-review flagged after they moved to their own
module. The kind now clears, and undecided file outlines on the corpus went
from 106 to 45. Nothing is asked again.

matching_windows walks seed pairs from an iterator of its own, which the
self-review found hidden under three loops; every corpus project's requests
are unchanged.
The split Score reads a large document's headings alone, and in every
labeled split finding it took one subject for several: dated release plans,
READMEs, the RealWorld frontend instructions and a list of business rules.
The kind, asked until now only when the split stayed undecided, is asked of
split findings too, and gains a plan for one change and requirements; a kind
that serves one subject clears the finding before its part is located. On
the corpus 14 split considers are gone, for about $0.002 of kind requests.

The large-docs note says a document "may mainly record" past work.
Planning built every unit's trace, recheck and settles up front and held
them as JSON values; most are never sent. On laravel/framework's 3,000 PHP
files the security follow-ups came to 580 MB of JSON, and a dry run peaked
at 3.2 GB for the security rules and 4.1 GB for all rules. A FollowUp keeps
the request's text and reads it back when the follow-up is asked: the peaks
are 1.5 GB and 2.2 GB. serde_json reads the text back to the same value, and
a run of all 103 corpus projects paid for no request.
A comment undecided after its first pass and recheck is asked what kind of
comment it is, and only a kind at 0.80 decided it. Step headings such as
`// update any single tag` above `this.addTag()` stayed between the
thresholds on every ask: 1,153 comments on the corpus, each leaving its file
uncertain. The kind is the last ask, so a leaning kind now decides: toward a
kind a reader could do without, a note, else clear. Undecided units on the
corpus went from 2.24% to 1.50% of judged units; nothing is asked again.
The internal-details check sees a test and the signatures it calls, and read
a debug panel's recorded queries (`panel._queries`, which the panel renders),
Devise's documented hooks and an app's state after an action as internals:
49 of 66 such considers labeled on the corpus were wrong. What the
assertions read separated them: none of the 44 reading state or effects was
right, and 16 of the 19 reading stored input or the program's own calls
were.

A consider from that check is now asked, with the recheck's evidence, what
its assertions read: results, state or effects clear it, stored input or
own calls keep it, and a split answer leaves a note. A test that reads
members through reflection or a cast to `any` keeps its consider unasked:
the question read those as results. The recheck's request is unchanged.

Labeled tests/value considers went from 29% to 77% right, and on held-out
projects from 2 right and 13 wrong to 1 wrong, for about $0.006.
The probe for instruction files that ignore files hide asked whether
`AGENTS.md` exists in each directory. On a case-insensitive file system
that also opens `agents.md`: refined-github keeps its instructions there,
and a React Native template keeps `claude.md`. The file was then read under
the probed name, the canonical path differed, the read failed as a
symlinked path, and the whole run ended incomplete with exit 2. The probe
now requires a directory entry of exactly that name.
A symlinked instruction file loads its target's text, but only when the
target was itself an instruction file by name. refined-github's `CLAUDE.md`
links to `agents.md`, so Claude Code's instructions were neither judged nor
counted toward what a session loads. Link targets are read too, and a target
no harness reads by its own name takes the link's readers: its findings name
the file itself and its text is counted once. Requests change only on the two
corpus projects with such files.
Copies between packages with no local dependency path between them are not
compared, but only Node, Rust and Python manifests named a package. Online
Boutique's Go services are each their own module, and 9 of the 10 copies
found between them, labeled on this audit's fresh projects, were wrong. A
module's path is its name and its `require` lines its dependencies, so
modules that share a local module are still compared.
JevGate never read server templates, and they held most of the documented
vulnerabilities it missed in the intentionally vulnerable apps.

- An ERB, EJS, JSP, Handlebars, Mustache, Nunjucks, Twig, Jinja or Go
  template, or HTML under templates/, views/, layouts/, partials/ or
  includes/, is parsed as its inline scripts with its tags blanked, and its
  top-level script is judged like a PHP page script. Its requests say the
  code runs in the visitor's browser.
- Its `template code` is one more unit: tags that write request, cookie,
  session or signed-in-user data unescaped (judged by injection), and every
  scriptlet and declaration of a JSP page once one reads the request
  (judged by every security rule).
- A Node handler that renders a view by name is sent the view's unescaped
  lines, as Django views are, and its questions name such templates.

A template holding neither inline scripts nor template code is not
selected. On the corpus, 24 documented vulnerabilities are found that no
rule read (RailsGoat's and DVNA's XSS, DVGA's paste-page XSS, DVJA's JSP
XSS, JavaVulnerableLab's JSP-only injections, traversal, SSRF and leaked
stack traces); 42 of 49 labeled reviews and 16 of 25 considers in templates
were right. Requests outside templates and render calls are unchanged on
the other 101 projects.
…check's help

Pug writes a value unescaped with `!=` right after a tag or at the start of
a line, and with `!{…}`; `err.name !== 'AbortError'` in a view's script
is a comparison. hackathon-starter's handlers were sent such comparisons as
unescaped output. `check --help` names the component and server template
files JevGate selects.
@tauanbinato
tauanbinato merged commit 4bfe7ed into main Sep 26, 2026
9 checks passed
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