Aikido's Top 10 App Security Problems, Mapped to DaloyJS (and the One Gap We Just Closed)
Aikido's 'Top 10 App Security Problems' is the short, blunt version of the OWASP list — SQLi, XSS, SSRF, path traversal, XXE, deserialization, shell injection, LFI, prototype pollution, open redirects. Here's the honest per-item mapping of what a DaloyJS app already blocks by default, what one opt-in line adds, and the single gap we shipped a new helper for in 0.35.0: safeRedirect().
A reader pinged me with Aikido's "Top 10 App Security Problems" post and the same question I get every time one of these lists makes the rounds: are we doing anything about this?It's a fair question. The post is a no-nonsense run-down — SQL/NoSQL injection, XSS, SSRF, path traversal, XXE, deserialization, shell injection, LFI, prototype pollution, open redirects. The basics. The stuff that's been on every top-10 list since 2007 and still ships in production CVEs every week.
I went down the list with our framework open in another window. The honest result: nine out of ten are already covered — most of them by default, a couple with a single opt-in line. One — open redirects— was a real gap. So I shipped a helper for it. You'll see it below as safeRedirect() in 0.35.0.
Below is the per-item map. No marketing voice, no "trust us, we're secure." Just what the constructor gives you, what you opt into, and what stays your problem.
#1 — SQL & NoSQL injection
- DaloyJS ships
- Standard Schema validation (Zod / Valibot / ArkType) with .strict() rejects unknown keys at the request boundary. assertNoMongoOperators() refuses $-prefixed keys in user bodies. CI gate verify:no-encoded-payloads catches base64-blob injection at PR time.
- You still own
- Use a parameterized query / prepared statement library (postgres.js, mysql2, Prisma) — DaloyJS isn't an ORM and never builds query strings for you.
The injection itself happens at your database driver, not at the HTTP layer — so this is half-shared. What DaloyJS does is make the two classic Mongo-flavored payloads impossible to pass through the request boundary unnoticed:
For SQL, the framework's contribution is that validated input comes out as the type you declared. A schema that expects email: z.string().email() will not let email = " OR 1=1 --"reach your handler looking like a string. You still have to call the driver correctly — but you don't get to claim you concatenated a string "because the type system told you to."
#2 — Cross-site scripting (XSS)
- DaloyJS ships
- secureHeaders() (on the moment you call it) emits a strict CSP with per-request nonces and Trusted Types. JSON responses ship with X-Content-Type-Options: nosniff. The built-in /docs HTML page uses escapeHtml() on every interpolation.
- You still own
- Sanitize HTML you intentionally render (DOMPurify, sanitize-html). DaloyJS is an API framework — when you do render markup, use the right escaper.
The default response shape is JSON. JSON does not execute. The risk window is your dynamic HTML routes, your SSR layer, and your front-end framework — and Daloy's job there is to make sure the browser's defenses (CSP, Trusted Types, nosniff) are on by the time you start rendering. They are.
#3 — Server-side request forgery (SSRF)
- DaloyJS ships
- fetchGuard() wraps globalThis.fetch with an allow-list and a default block list covering 169.254.169.254 (AWS/GCP metadata), 127/8, 10/8, 172.16/12, 192.168/16, ::1, fc00::/7. Redirects are followed manually with re-validation at every hop.
- You still own
- Add the explicit allow-list of upstream hostnames your service may call. One line.
This is the one I'm proudest of, because the most-quoted SSRF CVEs of the last five years — Capital One, Shopify, plenty of others — would have failed against a re-validating fetch wrapper. Daloy's does re-validate every hop. A 302 to 169.254.169.254 is just as dead as a direct one.
#4 — Path traversal
- DaloyJS ships
- The router rejects encoded traversal sequences (..%2F, %252F, NUL bytes) before any handler runs. assertSafeRelativePath() and sanitizeFilename() are exported for the rare handler that legitimately accepts paths.
- You still own
- Don't store secrets in /public or /static. Don't roll your own static file server.
#5 — XML external entity (XXE)
- DaloyJS ships
- DaloyJS does not parse XML. There is no built-in XML body parser to misconfigure. SAML, SOAP, and similar payloads are a dedicated library's job.
- You still own
- If you must parse XML (SAML auth flows, legacy SOAP), pick a parser that disables external DTD resolution by default — fast-xml-parser, libxmljs2 with the explicit option, or xmldom with documented hardening.
The cleanest defense against XXE is not parsing XML. Daloy doesn't. If your domain forces you to, the framework doesn't silently help — which is the right kind of unhelpful.
#6 — Insecure deserialization
- DaloyJS ships
- Bodies are JSON-only. The JSON parser strips __proto__ / constructor / prototype keys before validation. Cookies default to __Host- prefix + HttpOnly + SameSite=Lax. Session payloads are MAC'd (timing-safe verify) and rotate signing keys cleanly.
- You still own
- Don't accept Java-style serialized blobs, BSON from untrusted sources, or YAML !!js/function tags. If you do, validate the shape with a Standard Schema before touching it.
#7 — Shell / command injection
- DaloyJS ships
- The framework never spawns a shell. The CI gate verify:no-remote-exec refuses curl|sh-style installers in dependencies. verify:no-vulnerable-sandboxes blocks vm2-class libraries. The Aikido article's own recommendation — child_process.execFile() with array args — is the pattern we point you at in /docs/security/command-injection.
- You still own
- If your handler needs to run a binary, use execFile() with an args array. Never spawn('sh', ['-c', userInput]).
#8 — Local file inclusion (LFI)
- DaloyJS ships
- Same primitives as #4 (assertSafeRelativePath, sanitizeFilename) plus the structural fact that DaloyJS has no dynamic-template loader, no eval(), no Function() constructor pattern, and no require(userInput) anywhere on the request path.
- You still own
- Don't write your own template loader that dynamically resolves user-supplied paths. If you must, use an allow-list.
#9 — Prototype pollution
- DaloyJS ships
- The body parser, query parser, and cookie parser all strip __proto__ / constructor / prototype keys. isForbiddenObjectKey() is exported so middleware authors can do the same. Stripped keys are logged at debug level — silent removal would let the bug hide.
- You still own
- Don't write your own deep-merge helper. If you need one, lodash >=4.17.21 with the patched merge is fine.
#10 — Open redirects
- DaloyJS ships
- As of 0.35.0: safeRedirect(target, { allowedPaths, allowedOrigins, fallback }). Refuses //evil.com, /\\evil.com, javascript:, control-character response-splitting, off-origin absolute URLs, and unparseable input. Defaults to 303 + Cache-Control: no-store.
- You still own
- Pass the explicit allow-list. The helper will not let you publish a redirect helper with no allow-list and no fallback — that combination throws OpenRedirectBlockedError at use time.
This is the one I had to actually ship. Before 0.35.0, if you wanted to redirect from a Daloy handler you wrote something like:
That puts a load-bearing security decision on the developer at the latest moment in the stack. We have a verb for that: insecure default. So I wrote the missing helper, and gave it the only defaults that make sense — refuse on bad input, fallback if you ask for one, no implicit allow-list:
The helper is a small, self-contained module — no framework internals, no dependency on App or Context. You can use it from a handler, from a hook, from a custom adapter, even from a script. The validation rules are tested in tests/safe-redirect.test.tsand cover every bypass the article mentions plus a few it doesn't: backslash-prefixed paths, CR/LF response-splitting payloads, scheme spoofing, unparseable absolute URLs, and unsafe fallbacks (yes, the helper also refuses a fallback that is itself an open-redirect bait).
The honest scoreboard
Against Aikido's top 10: nine were already covered, one (#10) was a real gap and now isn't. The shared-responsibility line stays where it always was — the framework gives you the primitives and the defaults, and you don't get to claim you were "just writing a redirect handler" anymore.
If you're upgrading, safeRedirect is exported from the package root:
Related reading on this blog: Secure by Default, Cloud Security Architecture, Mapped to DaloyJS, Vibe Coding Security, CSRF in 2026. Or jump straight to /docs/security/fetch-guard and /docs/security/owasp-api-top-10.