Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/audit/_preamble.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Shared preamble — the security auditor

Read `SECURITY.md` first: it states what the packages guarantee, what the
consuming application owns, what is not defended, and the `FAIL IF` conditions
this run executes. Your scope is exactly the files listed under **Scope** in
your own prompt file.

For each `FAIL IF` in your scope, run the mechanical check — a test suite, a
script, a grep, a file read — and record PASS or FAIL with concrete evidence:
file path and line number, or command output. A `FAIL IF` bullet asserts
several properties in one sentence; **each clause gets its own verdict and its
own evidence**. Never satisfy a bullet in bulk.

Then do the qualitative pass described in your own file, rating findings
BLOCKER / WARNING / INFO. Report what you can prove. Use `UNVERIFIABLE` only
for a check you could not determine — a container that would not start, a
transient network error, or an area you ran out of room to reach — and say
which it was. It is never a substitute for a check you could have run.

Where `SECURITY.md` says a risk is accepted ("What is not defended"), do not
re-report it as a finding — report only if the situation has changed or is
worse than described.

Write your findings to the file named in your own prompt, and write them **as
you determine them — never buffered in your context for one write-up at the
end.** What is in that file is the whole of what the audit publishes: a run
that holds its results for a final write-up it never reaches publishes nothing.
Open the file before your first check:

```sh
printf 'VERDICT: INCONCLUSIVE\n\n### FAIL IF results\n\n' > <your report>
```

Then append each check's line as you determine it, and each finding as you rate
it, under `### FAIL IF results` (one line per check) and `### Qualitative
findings` (severity-tagged). **Append; never rewrite the file whole.**

**Its very first line must be literally `VERDICT: PASS`, `VERDICT: FAIL`, or
`VERDICT: INCONCLUSIVE`** — nothing else on that line. The reporting step reads
it, so it is the one part of your report a machine reads. It opens as
`INCONCLUSIVE` so a report you never finish fails closed on its own. Rewrite
that one line at the end, with Edit rather than `sed -i` (whose in-place flag
differs between GNU and BSD), then close the file:

```sh
printf '\n<!-- END OF REPORT -->\n' >> <your report>
```

**That sentinel is what tells the reporting step your report is finished**, so
write it last, once, and only when the verdict line above it is the one you
reached. A report that exists is a report still being filled in; a report
without the sentinel is read as inconclusive however its verdict line reads —
except a `VERDICT: FAIL`, which is a finding whether or not you finished.

Return `FAIL` if any `FAIL IF` in your scope is violated or any qualitative
finding is BLOCKER. Otherwise `INCONCLUSIVE` if any check is `UNVERIFIABLE` or
unfinished; `PASS` only when every check was determined.

Never print a secret value. Do not run `printenv` or `set -x`, and do not paste
the contents of a credential file into your report — report its mode and
location instead. This repository is public, and both your report and the
session transcript are world-readable.
97 changes: 97 additions & 0 deletions .github/audit/security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Domain: security

**Scope — this file, and no other:**

- `SECURITY.md`

**Output file:** `audit-report.md`

This is a code-and-policy audit of what the packed packages guarantee to a
consuming application. Everything `SECURITY.md` assigns to the application —
TLS, the origin gate, page CSP, secret storage, database provisioning, provider
registration — is out of scope; so are `@pgstencil/stripe` and the original
code/link `Auth` exports, which carry no rules yet.

## Mechanical pass

Run every `FAIL IF` in `SECURITY.md`. Most of them are pinned by a named test,
so start by running the suites and reading their assertions, then read the code
each bullet names.

```sh
pnpm db:verify # migrations apply and match the committed schema
pnpm test:unit # diagnostics allowlist and the other unit rules
pnpm test:better-auth # the session, CSRF, email-code, OAuth and linking suites
pnpm packages:verify # packs, installs into a clean project, checks provenance
```

`test:better-auth` and `packages:verify` need a working Docker daemon (see
`compose.yaml`). If the daemon is unreachable, every check those suites pin is
`UNVERIFIABLE` — say so and say why. **Never record a check as PASS on the
strength of a test you did not run**, and never quietly skip one.

Each `Pinned by` line at the end of a `SECURITY.md` section names the tests for
that section. Confirm each named test still exists and still asserts what the
bullet claims: a test that was renamed, skipped, or reduced to a smoke check
leaves its bullet unenforced, and that is a finding even when the code is fine.

The remaining bullets are read rather than run:

- The `Continuous checks` bullets are grep-and-read over
`.github/workflows/check.yml` and `.github/workflows/security-audit.yml`.
Check the triggers, the job names, `persist-credentials: false`, the
permission blocks, the reporting step's verdict grammar, and that the
redaction step is present and covers every file the archive step publishes.
- The `Packed provenance` bullets are read over `scripts/build-packages.ts`,
`scripts/pack-packages.ts` and `scripts/verify-packages.ts`, with
`pnpm packages:verify` as the evidence.

## Qualitative pass

Be adversarial, and go past the `FAIL IF` list. Read
`packages/auth/src/better-auth*.ts`, `packages/pgstencil/src/*.ts`, and the
suites under `tests/` — including what they do _not_ cover. Ask at least:

- **Can a caller influence where an OAuth callback lands, or reuse state across
applications?** Trace `oauthRequest` in `better-auth-oauth.ts` from the start
of the flow through the signed state cookie, the `oauth_flows` row, and the
atomic claim in `003_oauth_claims.sql`. Every parameter beyond the provider
name must come from server configuration; a state row must bind the browser,
the provider, and one application.
- **Does `verifiedOidc` cover every enabled provider?** Compare the providers in
`socialProviders` against the ones `verifiedOidc` and `providerSubject` in
`better-auth-email.ts` actually verify. A provider that is enabled but falls
through the verification switch signs users in on an unverified assertion.
- **Can a real user register an address in the `identity.pgstencil.invalid`
namespace?** Trace every writer of a user email — sign-up, linking,
`allowMissingEmail` account creation, and any profile update — against
`isIdentityEmail`. A reserved namespace that is only filtered on the way out
is not reserved.
- **Can a raw upstream error carrying a token reach diagnostics?** Follow a
provider or database error from where it is thrown to `diagnostic` and
`diagnosticError` in `packages/pgstencil/src/diagnostics.ts`, and to
`onAPIError`. Look for a path that stringifies an exception, a response body,
or a URL with a query string.
- **Is any rate-limit key derivable without the application secret?** Read
`keyed` and `consume` in `better-auth-security.ts`. A key an attacker can
compute lets them exhaust another address's budget, and one that varies with
something the caller controls lets them escape their own.
- **Can `@pgstencil/auth/better-auth-testing` be reached from a production
import graph?** Start at the package's `exports` map, then at
`examples/better-auth/src/worker.ts` and the esbuild `inject` that only test
bundles carry. A conditional import, a re-export, or a bundler that keeps the
module is the finding, not the presence of the file.
- **In `packages/pgstencil/src/database.ts` and `paths.ts`, is any shell or
Compose invocation built from a path or environment value a project could
control?** `PGSTENCIL_PROJECT_ROOT`, the derived Compose project name, the
`pgstencil.json` migrations path, and the bundled `compose.yaml` fallback all
cross into a child process. Look for string-built commands, unquoted
interpolation, and a path that escapes the project root.
- **What do the integration tests not cover?** Name the gaps. A guarantee whose
only evidence is that nobody has attacked it is worth saying out loud.
- **Have `SECURITY.md` and the code drifted?** Say which side is wrong. A bullet
that describes a control the code no longer has is a BLOCKER; a control the
code has that the file does not claim is an INFO.

Rate every finding BLOCKER, WARNING or INFO, and give each one a file path and
line number.
1 change: 1 addition & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
- run: pnpm format:check
- run: pnpm db:verify
- run: pnpm typecheck
- run: pnpm test:scripts
- run: pnpm test
- run: pnpm --filter @pgstencil/example-workers build
- run: pnpm packages:verify
Expand Down
Loading
Loading