Security
The vulnerabilities popweb is tested against.
@popcomputer/web’s security suite attacks the framework the way published vulnerabilities attacked others: 70 advisories from 25 frameworks and libraries, among them Next.js, Laravel, Hono, Better Auth, Rails, Django and SvelteKit. Each test names the advisories it guards against, and the package’s publish step runs every test, stopping if one fails. A passing test shows popweb isn’t affected by that bug as it was written, not that the framework can’t have bugs of its own.
How the suite works
Each file in the framework’s tests/security folder attacks one class of bug the way it attacked another framework, and its opening comment names the advisories it guards against. Most run against a small app that says, for every record, who may load it, and logs every statement its database receives, so a test can check both what a request got back and what it changed. A test there that fails is a vulnerability, not a style regression.
Below, each class has a short account of how the bug works and how popweb settles it, then a line for each advisory: the project it hit, and what went wrong there. A passing test shows popweb isn’t affected by the bug as the test writes it, and where a test covers only part of an advisory, its section says so. It doesn’t show the framework has no bugs nobody has found yet, and it doesn’t test your app’s own rules.
Where the advisories come from
The 70 advisories come from 25 projects. Most are web frameworks; some are libraries frameworks are built from, such as qs, Zod and Drizzle. Two of the tests also come from this framework’s own past: 1.3.0 served an upload typed image/png;x=,text/html back as a page, and stored publicly a response that an API key in a header of its own had signed in. Both were fixed in 1.4.0.
- Hono: 13 advisories
- Better Auth: 7 advisories
- Laravel: 6 advisories
- Next.js: 6 advisories
- Django: 4 advisories
- Rails: 4 advisories
- SvelteKit: 4 advisories
- Nuxt: 3 advisories
- React Router: 3 advisories
- Fastify: 2 advisories
- Koa: 2 advisories
- qs: 2 advisories
- Starlette: 2 advisories
- Drizzle: 1 advisory
- Effect: 1 advisory
- Express: 1 advisory
- FastAPI: 1 advisory
- Gin: 1 advisory
- Livewire: 1 advisory
- NextAuth.js: 1 advisory
- path-to-regexp: 1 advisory
- Phoenix: 1 advisory
- TypeORM: 1 advisory
- Valibot: 1 advisory
- Zod: 1 advisory
Another spelling of a protected path
A guard that matches a path as text, and a router that decodes it, can disagree. An encoded slash, a doubled one or a capital letter then reaches the page while the guard looks the other way.
popweb doesn’t decide who may see a record by the path’s text. The route that names a record says who may load it, and the lookup’s own query enforces that, so whichever spelling the router accepts, another member or a visitor gets nothing. The test tries fifteen spellings of one record’s path, among them /PROJECTS/p1, //projects/p1, /projects%2Fp1 and /projects/x/../p1, and seven of a guarded organisation’s.
- CVE-2026-29045 Hono: the router and serveStatic decoded %2F differently, so a protected file could be read past its middleware.
- CVE-2026-39407 Hono: repeated slashes in a path got a request to serveStatic past its middleware.
- CVE-2026-53721 Nuxt: route rules matched paths case-sensitively and the router didn’t, so another case got past their middleware.
- Tested in tests/security/requests.test.ts.
Headers that tell a framework about the request
Some frameworks act on headers a client can write: one that says the request came from middleware, one that asks for different page data, one that names the host. Whoever sends the header decides what the framework believes.
The test sends thirteen such headers, among them x-middleware-subrequest, Inertia’s own headers, method overrides, X-Original-URL and X-Forwarded-Host. None lets a visitor or another member load a record they couldn’t, none makes a shared cache keep a response, a method override changes nothing another member may not change, and no redirect names a host a header gave.
- CVE-2025-29927 Next.js: an x-middleware-subrequest header made it skip middleware, authorisation checks included.
- CVE-2025-43865 React Router: a header sent by the client replaced a page’s loader data.
- CVE-2026-27959 Koa: the hostname it reported came from Host and X-Forwarded-Host headers a client can set.
- CVE-2026-3635 Fastify: the host and protocol it reported could be set with X-Forwarded-Host and X-Forwarded-Proto from any connection.
- Tested in tests/security/requests.test.ts.
Writes from another site
A page on another site can post a form to yours, and the visitor’s browser sends your cookies with it. Frameworks check where such a write came from, and the checks have been fooled by a body type they didn’t expect, a type in capitals, Origin: null, and methods they forgot.
popweb refuses a write from another site before its route is looked up. The test sends nine body types and spellings, no type at all among them, from four other origins, null included, and every method that changes something. Each is refused with a 403 and writes nothing, and so is a write whose only Referer names another site.
- CVE-2024-48913 Hono: a write with no Content-Type passed its CSRF check.
- CVE-2024-43787 Hono: a Content-Type in capitals passed its CSRF check, which compared it case-sensitively.
- CVE-2021-32677 FastAPI: it read JSON from a body sent as text/plain, which a form on another site can send.
- CVE-2023-29003 SvelteKit: a write with a Content-Type its CSRF check didn’t list, such as text/plain, got past it.
- CVE-2023-29008 SvelteKit: the same check was then passed by a type in capitals, such as text/plaiN.
- CVE-2026-27978 Next.js: Origin: null got past the CSRF check on Server Actions, read as no Origin at all.
- CVE-2026-53663 React Router: PUT, PATCH and DELETE requests weren’t checked for forgery.
- Tested in tests/security/cross-site.test.ts.
Fields a form shouldn’t accept
Mass assignment is a form setting a field it never offered, such as who owns a record. Its cousins reach further: a key that names the object prototype changes every object the server holds.
In popweb each form is a schema, and only the fields it declares reach your code. The test posts owner, id and visibility fields in every spelling the advisories used, prototype keys in JSON, forms, multipart bodies and the query string, keys named after the methods every object inherits, and body values that try to swap the record the path names. None changes a row it shouldn’t, and none reaches the prototype. An array or an object where the form takes text is refused.
- CVE-2020-24941 Laravel: a key written foo->bar set a guarded JSON column.
- GHSA-44pg-c29v-hp6r Laravel: a key written users.name was stripped to name, getting past the model’s guarded fields.
- CVE-2020-8164 Rails: permitted parameters were returned unfiltered, getting past Strong Parameters.
- CVE-2025-60542 TypeORM: a field sent as city[role]=admin set another column, through SQL injection in save and update.
- CVE-2022-24999 qs: a __proto__ key in parsed input polluted the object prototype.
- CVE-2026-56763 Hono: a __proto__ key in a body parsed with dot notation polluted the object prototype.
- CVE-2017-1000048 qs: a bracketed key got past its guard on the names Object.prototype holds.
- CVE-2026-84504 Fastify: a body with a value property had that property replace the validated body.
- CVE-2026-44574 Next.js: a query parameter changed the route parameter a page received, past its middleware.
- Tested in tests/security/input.test.ts.
Uploads served back as pages
A file a visitor uploads is served from your app’s own address. If a browser reads it as a page, its script runs as your app. The checks have been fooled by a type the client chose, an SVG sent as an image, and a type that’s really a list, which a browser reads by its last entry.
In popweb an upload must be one media type, and markup sent as an image is refused, whatever its first bytes claim. A served file whose type isn’t one media type is sent as plain bytes. Anything but an image, a PDF, plain text, JSON, audio or video is shown only inside a sandbox, so its script can’t run as your app, and a download’s name is written as one quoted value, however it tries to break out of its header.
- CVE-2024-47823 Livewire: an upload’s extension was guessed from the MIME type the client sent.
- CVE-2018-16477 Rails: the type and disposition Active Storage served a file with could be changed, so an HTML file ran inline.
- CVE-2024-26144 Rails: a flaw in how Active Storage served uploaded files.
- CVE-2021-39178 Next.js: an SVG served through its image optimisation ran script.
- CVE-2022-36359 Django: a file name escaped its Content-Disposition header, for a reflected file download.
- CVE-2023-29401 Gin: FileAttachment didn’t sanitise a file name, so it could escape its Content-Disposition header.
- Tested in tests/security/served-content.test.ts.
Open redirects
Apps send people on after they sign in, or back to a form that failed, and the address often comes from the request. If it can name another site, a link on your own domain sends people to a lookalike. Checks have been fooled by //evil.com, which a browser reads as another host, by encoded and backslashed spellings, and by javascript: addresses, which run script instead of leaving.
In popweb the page to return to after signing in, and the page a failed form goes back to, are only ever the app’s own: the test tries fifteen spellings of another site. A redirect to a javascript: or data: address is refused, since an Inertia visit would run it. A path filled from a form’s fields leaves a value that isn’t one path segment, such as an empty one or .., unfilled, so the redirect stays on its route and site.
- CVE-2025-27143 Better Auth: a scheme-less address such as //evil.com passed its trusted-origin check.
- GHSA-vp58-j275-797x Better Auth: a crafted address got past its trusted origins, wildcard patterns included.
- CVE-2025-53535 Better Auth: a check that an address started with a trusted origin let another site through.
- CVE-2025-62595 Koa: back() read //evil.com as a path, and redirected there.
- CVE-2024-29041 Express: an encoded Location got past allowlists.
- CVE-2017-1000163 Phoenix: a redirect to a slash, a line break and a host left the site.
- CVE-2024-34343 Nuxt: navigateTo followed a javascript: address, running its script.
- CVE-2026-22029 React Router: a redirect to a javascript: address ran its script.
- Tested in tests/security/redirects.test.ts.
Page data breaking out of the HTML
Inertia, which popweb’s pages run on, writes each page’s data into the HTML as JSON inside a script tag. A value containing </script> can end that script early and start one of its own, and an HTML comment opener can swallow the rest of the page, which Inertia and inertia-laravel each fixed in a pull request.
The test writes seven such values into page data, as values and as keys, and checks that each reads back exactly and that the page gains no element.
- CVE-2025-32388 SvelteKit: search parameter names went into the boot script unescaped, so </script> could break out of it.
- Tested in tests/security/cookies-and-pages.test.ts.
Requests at once seeing each other’s user
One Worker serves many requests at the same time. If something about a request, such as its route parameters or who’s signed in, is kept where another request can read it, a request that pauses for the database can carry on as someone else.
The test sends forty requests at once from two members, each pausing between reading its user and loading its record, and each sees only its own. A lint rule also reports module-level state in your own code, which every request a Worker serves shares.
- CVE-2023-50710 Hono: requests running at once could share route parameters.
- CVE-2026-32887 Effect: a request’s context could be lost, or mixed with another’s, inside fibers under load.
- CVE-2026-59896 Hono: hono/jsx didn’t keep context apart per request, so one request’s data could reach another’s page.
- Tested in tests/security/cache-and-isolation.test.ts.
Bodies too big to read
A Worker shares its memory among the requests it serves, so one request that makes it read a huge body, or build thousands of error messages, slows or stops it for everyone. Frameworks have read bodies whole before anything could refuse them.
popweb reads at most 1 MB of a body unless an action asks for more, and Better Auth’s endpoints get the same limit. A body that declares a larger length is refused before a byte is read, and one sent without a length stops being read at the limit. A failed form lists at most 100 invalid fields, and a request of thousands of values stops at its first invalid one, so the answer is the same size whatever the count.
- CVE-2024-47874 Starlette: multipart form bodies were parsed without a limit.
- CVE-2026-54283 Starlette: form limits were ignored for urlencoded bodies.
- CVE-2026-44456 Hono: bodyLimit() could be passed by a body sent without a length.
- CVE-2026-40073 SvelteKit: the Node adapter’s body size limit could be passed.
- Tested in tests/security/sizes.test.ts.
Back after signing out
Inertia keeps each page’s data in the browser tab’s history, and Back shows it again without asking the server. After someone signs out, the next person at the same browser could page back through their account. Laravel apps guard against this with Inertia’s history encryption, cleared on sign-out. There’s no advisory id for this class: the precedent is how Laravel sets Inertia up.
popweb encrypts a signed-in person’s pages in the history by default, and the first page after the session ends clears them. The test ends the session every way it can end, by signing out, through Better Auth’s client, by being revoked elsewhere, by the cookie simply expiring, and by another member signing in, and checks that each clears the history or makes the browser ask the server. Tested in tests/security/history.test.ts.
Error detail in production
An error page that shows its message, its stack or its settings tells an attacker how your app is built, and sometimes hands over a secret.
In popweb an error in production shows no message, cause or stack to a browser or a JSON client, and nothing in the request, whether a query parameter, a header or a cookie, turns development on. The test checks development does show the detail, so its absence is production’s doing. Tools that collect errors, such as Sentry, get no submitted password, cookie or token, and no value from the address or Referer, such as a password-reset token.
- CVE-2021-3129 Laravel: debug mode’s error page, Ignition, could be reached and made to run code.
- CVE-2024-52301 Laravel: the environment, and so debug mode, could be set from the query string.
- CVE-2019-5420 Rails: the secret development mode generated could be guessed, letting an attacker run code.
- Tested in tests/security/production.test.ts.
The address sign-in limits count
Sign-in limits count guesses by the client’s address. If that address comes from X-Forwarded-For, which the client writes, every request can claim a new one.
popweb reads only Cloudflare’s CF-Connecting-IP, which a client can’t set; without it, the address is unknown, never a header’s. The test checks where the address comes from. Since 1.9.0 an IPv6 client counts by its network, its /64 unless an app gives another size, so it can’t send each guess from another of its addresses, which the Better Auth advisory is also about; the tests send guesses from one /64’s addresses until they’re refused.
- CVE-2026-45364 Better Auth: its rate limiter keyed on the X-Forwarded-For address as written, so IPv6 clients could rotate past it.
- CVE-2026-27700 Hono: behind AWS’s load balancer, its connection info took the first X-Forwarded-For address, which a client writes.
- Tested in tests/security/production.test.ts.
Slow regular expressions
Some regular expressions take exponentially longer as their input grows, so one long field can hold the thread that serves every request.
Twelve of popweb’s validators, from email and url to uuid, each get a hostile 20,000-character value and must answer within 50 milliseconds, and so must reading a media type and a route’s path. 1.5.0 made the email validators linear, where a 64 KB address had taken about 2.2 seconds, and a test checks they still accept and refuse exactly what they did.
- CVE-2023-4316 Zod: a regular expression could be made to backtrack exponentially.
- CVE-2025-66020 Valibot: its emoji regular expression could be made to backtrack exponentially.
- CVE-2024-45296 path-to-regexp: it produced regular expressions that backtrack, so a crafted path took exponential time.
- Tested in tests/security/production.test.ts.
Fixes in the libraries popweb uses
Some bugs live in the libraries popweb is built on. For those, the test doesn’t replay the attack: it checks that the versions popweb asks for start at the first release with the fix, Hono 4.13.5, Drizzle 0.45.2 and Better Auth 1.7.7, and leave out the last release without it.
Hono’s CVE-2026-56763, fixed in the same release, is listed under fields a form shouldn’t accept, where it’s also attacked directly.
- CVE-2026-29086 Hono: setCookie() didn’t sanitise domain and path, so attributes could be injected. Fixed in 4.13.5.
- CVE-2026-47675 Hono: the cookie helper didn’t sanitise sameSite and priority, so a Set-Cookie could be injected. Fixed in 4.13.5.
- CVE-2026-84364 Hono: unbounded dot-notation nesting in parseBody() could exhaust memory. Fixed in 4.13.5.
- CVE-2026-39356 Drizzle: SQL identifiers weren’t escaped properly, allowing SQL injection. Fixed in 0.45.2.
- GHSA-965c-763c-88jm Better Auth: OAuth state could be used as a magic link to sign in as another user. Fixed in 1.7.7.
- Tested in tests/security/production.test.ts.
An account registered with someone else’s address
If anyone can sign up with an address they don’t own, and the owner later signs in by an emailed link, the impostor’s password, session or two-step challenge can outlive the owner’s arrival.
popweb’s Better Auth template signs no one in by password until their address is proven, and the tests replay the attack: once the owner signs in by a link, a two-step challenge the registrant holds, or a password sign-in they started, signs nobody in, and a phone number sent with the sign-up is refused. One risk remains, which the auth docs describe: an owner who follows a verification link for an account they never created proves the address for whoever chose its password. These tests sit with the framework’s other Better Auth tests, and the suite’s README names them.
- CVE-2026-53516 Better Auth: an OAuth sign-in could link to an account someone had registered first with the owner’s unproven address.
- GHSA-qq9h-g4jm-xgf3 Better Auth: someone who registered the owner’s address first kept password access after the owner began signing in by magic link or emailed code.
- CVE-2022-35924 NextAuth.js: a magic link could be sent to an unwanted address.
- Tested in tests/better-auth/seam.test.ts.
What isn’t covered yet
One class has no guard yet, and the suite says so: gaps.test.ts lists it as a todo, so every run shows it. It’s SQL whose names, rather than its values, come from the request, such as a column to sort by given to Drizzle’s sql.identifier(), .as() or orderBy. The todo is for a lint rule that reports such a value. Its precedents are SQL injection advisories in Django (CVE-2022-28346 and CVE-2024-42005), Drizzle (CVE-2026-39356) and Kysely (CVE-2026-32763).
Drizzle’s own fix is in the release popweb asks for; what’s missing is the check on your code. Until it exists, if your app lets visitors choose a column to sort or filter by, match their choice against a fixed list of columns yourself.
The suite also can’t test your app’s own rules, such as who counts as an admin. How the security works lists what’s still yours.
Run the suite yourself
To run it, clone the framework’s repository and run bun install. The command bun test ./tests/security/ then runs the suite: as of 1.9.0, 85 tests in 11 files, 84 passing and one the todo above. The command bun test tests/better-auth/seam.test.ts runs the account registration tests with the rest of the Better Auth ones, and bun run test runs everything the publish step does.
Each test file opens with the advisories it guards against, so you can check every line on this page against the test that backs it.
Questions people ask
Is @popcomputer/web secure?
No framework can promise that, and this one doesn’t. What it offers is evidence you can check: 70 published vulnerabilities from other frameworks and libraries, each turned into a test that runs before every release; two security audits, whose findings 1.5.0 and 1.6.0 fixed; and the protections How the security works describes, on in every app by default. Your app’s own rules, such as who counts as an admin, are still yours to get right.
Has @popcomputer/web been audited?
Twice. 1.5.0 fixed what a security audit of 1.4.2 found: one high, five medium and eleven low findings, and ten of its eleven leads; the eleventh needed no change. 1.6.0 fixed what a second audit found in 1.5.0’s changes: one medium and sixteen low findings, and six leads. Each fix was proven by re-running the audit’s own reproductions on the release before and the release after. 1.7.0 then fixed four more issues, found by the framework’s own fuzz tests and models of the state it keeps between requests. The changelog lists every one.
What happens when a new vulnerability is published in another framework?
There’s no published policy or timetable for it. What the suite shows so far: a class the framework doesn’t yet guard is listed as a todo, so every run shows it, and becomes a test once the guard exists, as the body size limit and accounts registered with someone else’s address did in 1.5.0. When a library popweb builds on fixes a bug in a part it uses, the versions popweb asks for move to the fixed release, and a test checks them, as it does for Hono, Drizzle and Better Auth.
Is @popcomputer/web production ready?
It’s at 1.9.0 and follows semantic versioning, and each release’s changelog entry says what an app needs to change, with an upgrading guide for each step. 1.6.0 shipped with 1,449 passing tests. 1.0 came out on 14 October 2026, so it’s new; Popcomputer and The East Anglia Software Company maintain and support it, and this site runs on it. Weigh that against what your app holds, and read the changelog’s security sections before you decide.
Does a passing suite mean my app can’t be hacked?
No. A passing test shows popweb isn’t affected by that bug as it was written. It can’t show the framework has no bugs nobody has found yet, and it doesn’t test your app’s own rules, such as who may see an admin page. Vibe coding safely covers how to check what your AI writes.