Skip to content

Tags: Zyoffsec/airtight-secure-coding

Tags

v0.3.2

Toggle v0.3.2's commit message
fix: ten defects found by watching the guard read realistic code

Two of them were stalls, which matter most. A guard that hangs holds up every
write in the session, and the developer cannot tell it apart from a hung editor.
A single 150 KB line of ordinary text, and a line repeating the word PASSWORD,
each took the guard past a minute. Both came from an unbounded quantifier beside
an alternation — under IGNORECASE `[A-Z_]*` matches lowercase too, so it tried
every start position with a long greedy match and backed out of each. Bounded
now, with seven hostile inputs in --selftest under a five-second budget.

The rest were the guard misreading correct work:

  f"built from {path}" read as SQL, because SELECT, FROM, WHERE, UPDATE and
  DELETE are ordinary English words. Interpolation now only counts inside a
  string carrying a SQL shape — SELECT … FROM, INSERT INTO, UPDATE … SET.

  r.Use(RequireSession) and router.use(requireSignedIn) read as open routes,
  because authentication was only recognised in snake_case.

  {"httponly": True, "secure": True} splatted into set_cookie read as flags
  missing entirely, because of the quote before the colon.

  Multi-line SQL escaped the injection check, which is where any query longer
  than one line actually lives.

  The guard denied its own implementation, whose self-test corpus necessarily
  contains an example of everything it detects.

And three surfaces that are routes without looking like one — WebSocket
endpoints, GraphQL resolvers, SvelteKit load — plus PHP's shell family, with
escapeshellarg recognised as the remedy rather than flagged as the problem.

--selftest grows from 97 cases to 145. Two of the late fixes were themselves
caught by it before they shipped: escapeshellarg denied as injection, and a
static insertAdjacentHTML denied because `,\s*(?!…)` backtracks to zero spaces
and applies the lookahead to whitespace. A suite that catches its author's
regressions is the whole point of having one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UcUisdbA7yYw8LDmkndyxk

v0.3.1

Toggle v0.3.1's commit message
fix: the guard denied correctly written SQL

Found within a day of release, on a read-only reporting tool whose queries were
properly parameterised: a static SELECT count(*), and a WHERE id BETWEEN ? AND ?
with both values bound. Both were refused.

The interpolation check scanned a 200-character window after the SQL keyword.
That window ran past the end of the statement and picked up the
f"conv {cid} | {lang}" of an unrelated print on the next line, then read it as a
substitution inside the query above. strftime('%s', ?) tripped it too — SQLite's
format specifier read as Python's.

Every alternative now stays inside a single string literal, so neighbouring code
cannot contribute a match. Real injection is unaffected: f-strings, %, +,
.format(), template literals, a query built into a variable, and
$queryRawUnsafe all still deny.

This is the failure mode that matters most. A guard that blocks correct work does
not get argued with — it gets switched off, and then it protects nothing. Eight
cases pinning both the false positives and the real injections are now in
--selftest, which runs 97.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UcUisdbA7yYw8LDmkndyxk

v0.3.0

Toggle v0.3.0's commit message
feat: v0.3.0 — ship the guard, and ship it as a plugin

The gates were never the problem; reaching for them was. Measured across six
runs of one backend prompt, the skill loaded twice. The four runs it sat out
produced passwords under SHA-256 and an endpoint that returned every user's
orders to anyone who asked.

So this release adds the half that does not depend on remembering.

hooks/ — a pre-write guard that denies fifteen failures it can prove: open
routes, IDOR, missing CSRF tokens, mutations behind GET, unverified webhooks,
queries and shell commands built by interpolation, raw HTML sinks, any-origin
CORS with credentials, session cookies without their flags, passwords under
fast hashes, credential endpoints with no bound, and three shapes of leaked
secret. It runs on the write, so a long session cannot dilute it, and it asks
for the registry by name the first time a session touches a security surface.

The entry point holds no logic — a SyntaxError in the implementation is caught
by the launcher's import, so a broken guard cannot take a session down. Verified
by injecting each fault. `--selftest` runs 89 cases; every false positive found
against real generated code is pinned as one, because a guard that blocks clean
work gets switched off.

Distribution is now a Claude Code plugin, which is the only way the guard
reaches a machine without someone editing a settings file by hand:

    /plugin marketplace add Zyoffsec/airtight-secure-coding
    /plugin install airtight

Both halves arrive together and updates follow on their own. That is the
platform updating what you installed through it — Airtight still fetches and
executes nothing, because doing so would be Gates 93 and 95.

Registry grows 67 -> 70: cross-site request forgery had no coverage at all.
Gates 130-132 in a new topic. No number was renamed or reused, so citations in
existing reports still resolve. The skill moved to skills/airtight/ because the
plugin layout requires it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UcUisdbA7yYw8LDmkndyxk