Security
Whatever a request sends can reach a query or a record through d1-record, so a mistake in it is a mistake in every app that uses it. Here’s what it protects against, what it leaves to your app, the tests that check each guarantee, how releases are built, and the security reviews it has had.
These are the project’s own reviews, not an independent audit. Each says what it covered and what it didn’t.
What d1-record protects against
- SQL injection. Every value is bound as a parameter, never written into SQL text. Identifiers come only from a Model’s declared attributes, and are quoted. Raw SQL is only possible through the
sqltagged template, which binds every value it’s given. - Values that aren’t what TypeScript says. A value from JSON, a form, or a query string is cast to its attribute’s type when it’s assigned or used in a condition, and anything ambiguous is refused:
"false"can’t savetrue. See Models. - Prototype pollution and inherited names. Attributes and associations are looked up only by the Model’s own names, so
__proto__,constructor, and other names every object has are unknown names, andObject.prototypeis never written to. - Secrets in logs and errors. Values of attributes named like passwords, tokens, keys, and emails are hidden from
onQuerylisteners and from error messages. See Logging. - One request’s data reaching another. Nothing about a request (a D1 Session, a listener) is ever stored on a Model class. See Database resolution.
- Denial of service through parsing. The patterns that read numbers and dates are linear, so a long value can’t use up a Worker’s CPU time.
What’s left to your app
- Choosing which fields a request may set, with
permit, destructuring, or a schema. See Untrusted input. - An untrusted
nullin a condition.findBy({ token: null })matches rows whose token is NULL. Check a value’s type before querying with it. - Authorization. Whether this user may read or change this record is your app’s decision.
- Formats and ranges. Casting checks types, not meaning: use validations.
- Secrets inside
sqlfragments. A fragment’s values have no attribute, so they’re not hidden from listeners.
Guarantees, and the tests that check them
The library’s guarantees are tested against a real D1 database (through Miniflare), and CI’s by a test that reads its workflows. CI runs every test on every change.
| Guarantee | Tests |
|---|---|
| Values are bound, identifiers are quoted, and plain strings are refused as SQL | sql-safety |
| Assigned and condition values are cast, or refused, in linear time | attribute-casting |
| Only a Model’s own names are attributes or associations | name-lookups |
permit keeps only the named attributes, reading own keys only | permit |
| Filtered attributes’ values never reach listeners or errors | filter-attributes |
| A request’s database stays with that request | current-database |
| CI’s actions are pinned, and release jobs run no install scripts | ci-workflows |
Supply chain
- No runtime dependencies in your Worker. The library imports no other package, so no other package’s compromise can reach your Worker through it. The command line, a development tool, depends on
jitito load your Models; installing the package installs it, and your Worker doesn’t bundle it. - Releases are built by CI. The npm package is built and published by the repository’s release workflow, never from a laptop, so what’s on npm is what’s in the repository. Publishing uses npm’s trusted publishing, so no npm token is stored anywhere. Version 0.1.0 was published before that was set up, and has no provenance attestation linking it to its commit (#109).
- CI is pinned. Every third-party GitHub Action is pinned to a commit, not a tag that could be moved, because a moved tag would run someone else’s code in the job that publishes. A test fails if one isn’t.
- Release jobs run no install scripts. The jobs that decide, build, version, and publish a release install dependencies without running their install scripts and without a shared cache, so a compromised development dependency can’t change what’s built or published.
- Dependencies are watched. Dependabot proposes updates weekly, waiting a week after each release, since a compromised release is usually caught within days.
pnpm auditis part of each security review.
Review log
Each security review adds an entry here: what it covered, how, what it found, and what it didn’t cover.
2026-10-04: the library, its command line, and CI
Scope: the library (src/), the command line (cli/), the CI and release workflows, and the development dependencies.
Method:
- a security review of the whole codebase with Sentry’s
security-reviewskill (OWASP-based), briefed on a library’s risks: what happens when an app passes untrusted input into it; - untrusted inputs probed against a real D1 database: prototype keys, wrong types,
null, long values; - a dependency audit with Trail of Bits’
supply-chain-risk-auditorandpnpm audit; - each fix then reviewed with Trail of Bits’
differential-review.
Findings, all fixed:
| Severity | Finding | Issue |
|---|---|---|
| High | Assigned values weren’t checked at runtime: "false" saved as true for a boolean | #81 |
| High | The publish job used actions pinned by movable tags, and ran install scripts | #80 |
| High | Found while fixing #81: a pattern that backtracked on long numbers (ReDoS) | #81 |
| Medium | No protection against mass assignment, and no guidance on an untrusted null | #82 |
| Medium | Bound values, secrets included, reached onQuery listeners and errors | #83 |
| Low | Inherited names (constructor, __proto__) caused errors instead of being unknown names | #84 |
| Low | Development dependency advisories, and unescaped config values in generated code | #85 |
Not covered: the source of the third-party Actions at their pinned commits; D1 and workerd themselves; isolation between concurrent requests beyond the existing tests; apps’ own code.
Reporting a vulnerability
Please report a security problem privately, as the security policy describes, never in a public issue.