SecurityWorth watching

Watch: Laur Spilca on Software Security for Developers (GOTO 2026), and What DaloyJS Already Decides for You

Laurentiu Spilca and Thomas Vitale spend a GOTO 2026 conversation on why developers avoid security, the eternal encoding-vs-hashing-vs-encryption confusion, the danger of reinventing crypto, AI writing code with no security awareness, and why PKI still matters. Here is the talk, plus an honest mapping of which of those problems a DaloyJS app already takes out of your hands.

Devlin DuldulaoFullstack cloud engineer9 min read
Software Security for Developers · Laur Spilca & Thomas Vitale · GOTO 2026. About 45 minutes, worth every one of them.

I have a rule: when a Java Champion who wrote a whole book on the subject sits down to explain why developers keep getting security wrong, I shut up and watch. Thomas Vitale interviews Laurentiu Spilca about his co-authored book Software Security for Developers, and even though it is a Java-flavored conversation, almost none of it is actually about Java. It is about the things we all keep messing up, regardless of language.

I have been writing backends for about ten years, the last few of them from a desk in Norway where the winters give you a lot of time to think about your past mistakes. Most of mine were security mistakes. So instead of a summary, here is the talk mapped against the thing I now build to stop myself from repeating those mistakes: DaloyJS. Where the framework already makes the decision for you, I will say so. Where it cannot, I will say that too.

1. Why developers avoid security

The panel is blunt about it: security feels like a tax. It is the part of the ticket with no demo, no dopamine, and a high chance of making you look slow. So it gets deferred to a sprint that never comes. I have done this. I have shipped the "we will add auth properly later" version and watched "later" turn into "never" until a pentest report turned it into "urgently."

My honest take is that you cannot motivate your way out of this. You have to make the secure path the lazy path. That is the entire design bet of DaloyJS. The dangerous defaults are off, and turning them back on is what costs you effort, not the other way around. And the parts that genuinely require discipline get moved into CI, so the pipeline stays motivated even when you do not.

bash
# The "developers avoid security because it feels like a tax" problem,
# answered with CI gates instead of good intentions. Every create-daloy
# project runs these on every PR. A failure blocks merge.
pnpm verify:secret-comparisons   # all secret compares use timingSafeEqual, not ===
pnpm verify:no-leaked-credentials
pnpm verify:no-remote-exec       # no curl|sh, no eval(fetch(...))
pnpm verify:no-lifecycle-scripts
pnpm verify:actions-pinned       # every GH Action pinned to a commit SHA

# The point: you don't have to FEEL motivated about security at 5pm on a
# Friday. The pipeline is motivated for you.

2. Encoding, hashing, encryption: pick the right word

This is the part of the talk I wish I could mail to my younger self. The confusion between encoding, hashing, and encryption is not pedantry, it is the root cause of an enormous number of real bugs. Base64 is not encryption. SHA-256 of a password with no salt is not password storage. "We encrypt the passwords" is, nine times out of ten, a sentence that means "we hashed them, badly, and we are not sure which."

Three lines, three different jobs, never interchangeable:

ts
// The confusion the talk keeps coming back to, in three lines.
//
// ENCODING: reversible, NO key. It is a costume, not a lock.
//   Anyone can take the costume off. Base64 is not "encrypted."
const encoded = Buffer.from("hunter2").toString("base64"); // aHVudGVyMg==
const back = Buffer.from(encoded, "base64").toString();    // "hunter2"  <- trivially reversed

// HASHING: one-way, no key, you cannot get the input back.
//   For PASSWORDS use a slow, salted KDF, never a bare SHA-256.
import { hash, verify } from "@daloyjs/core/password"; // argon2id under the hood
const stored = await hash("hunter2");          // $argon2id$v=19$m=...,t=...,p=...
const ok = await verify("hunter2", stored);    // true, constant-time compare

// ENCRYPTION: reversible, WITH a key. This is the actual lock.
import { seal, open } from "@daloyjs/core/crypto"; // AEAD (XChaCha20-Poly1305)
const box = await seal(secretKey, "card: 4111 1111 1111 1111");
const plain = await open(secretKey, box);

DaloyJS does not try to teach you the difference in a tooltip. It just refuses to hand you the wrong tool with a friendly name. Passwords go through a salted argon2id KDF. Secrets at rest go through AEAD with a key. There is no encrypt() helper that quietly base64-encodes and lets you tell your boss it is secure. The vocabulary is enforced by the API surface.

3. Do not reinvent established security standards

Spilca hammers this and he is right. Every time a developer writes their own token format, their own MAC, their own "fast" password hash, they are signing up to lose a fight that brilliant cryptographers spent decades winning. The famous JWT alg: none disaster exists because people hand-parsed tokens and forgot the one step that mattered: checking the signature.

So DaloyJS ships the boring standard versions and makes the custom path the awkward one. JWT verification goes through a vetted library with JWKS rotation. CSRF is double-submit plus Fetch-Metadata, which I wrote about in CSRF in 2026, not a scheme I invented. Sessions are signed cookies with the right prefixes already on.

ts
// "Don't reinvent established security standards." - the whole panel,
// repeatedly. DaloyJS does not give you a homemade token format, a custom
// MAC, or a "fast" password hash. It gives you the boring standard ones
// and makes the dangerous custom path the hard one to type.
import { App, jwt, session, csrf, secureHeaders } from "@daloyjs/core";

const app = new App();

// JWT verification uses a vetted library + JWKS rotation, not a
// hand-parsed "split on dots and base64-decode" routine that forgets
// to check the signature (the classic alg:none footgun).
app.use(jwt({ jwksUri: process.env.JWKS_URI! }));

// Sessions are signed + (optionally) encrypted cookies with the
// __Host- prefix, Secure, HttpOnly, SameSite=Lax already set.
app.use(session({ secret: process.env.SESSION_SECRET! }));

// CSRF is double-submit + Fetch-Metadata, not a token scheme you invented
// at 2am. See /blog/csrf-in-2026-double-submit-and-fetch-metadata.
app.use(csrf());

app.use(secureHeaders());

4. AI-generated code with no security awareness

This is the newest thread in the conversation and the one that actually keeps me up. The model is confident, fast, and completely unbothered by threat modeling. It will happily write a handler that reads any URL the prompt suggests, parses an unbounded body, and reflects an error straight back with the database hostname in it. And the reviewer, a real human, is tired at 5pm and the diff is 400 lines long.

You cannot out-discipline this with code review alone. The only thing that scales is assuming the handler was written by something that does not care, and putting the guardrails somewhere the model cannot skip: the constructor and the router.

ts
// "AI-generated code written without security awareness." This is the
// one I lose sleep over, because the model is confident and the reviewer
// is tired. DaloyJS assumes the handler was written by something that
// does not care, and puts the guardrails on the constructor:
new App();
//
//  - 1 MiB body cap + Content-Length check -> 413 (the model forgot limits)
//  - 30s request timeout (the model wrote a hung handler)
//  - __proto__ / constructor / prototype stripped from JSON (mass assignment)
//  - path traversal ('..' , '//', %2e%2e, %00) rejected before routing
//  - 5xx bodies redacted in production (no stack traces to the attacker)
//  - real 405 with Allow, not an enumeration-friendly 404
//  - cookies default __Host- + Secure + HttpOnly + SameSite=Lax
//
// And the schema IS the route, so the AI literally cannot ship an
// unvalidated body without a build-time error.

Because the schema isthe route in DaloyJS, an AI cannot ship an unvalidated body without producing a build-time error. That is the difference between "please remember to validate" and "the build is red until you do." I unpacked more of this in Vibe Coding Security.

5. PKI and certificates still matter

The closing theme is one a lot of web developers quietly hope is someone else's job: PKI, certificate chains, who-signed-what. In a world of service meshes, mTLS between services, and signed artifacts in your supply chain, "I just trust whatever the load balancer terminates" is not a strategy anymore.

DaloyJS does not invent its own transport, that would violate rule 3. It assumes a real PKI exists and gives you the request-side primitives that lean on it: client-certificate auth as first-party middleware, and a fetchGuard() that validates the upstream chain with no rejectUnauthorized: false escape hatch in the public API.

ts
// "Understanding PKI and certificates is more important than ever."
// DaloyJS does not invent its own transport. It runs behind real TLS and
// gives you the request-side primitives that assume a PKI exists:
//
//  - fetchGuard() does egress allow-listing AND validates the upstream
//    certificate chain (no "rejectUnauthorized: false" escape hatch in
//    the public API).
//  - mTLS client-cert auth is a first-party middleware, not a snowflake.
import { App, fetchGuard, clientCert } from "@daloyjs/core";

const app = new App();

// Mutual TLS: trust a CA, map the cert subject to a principal. The chain
// is verified by the platform's PKI, you don't parse ASN.1 by hand.
app.use(
  clientCert({
    ca: process.env.CLIENT_CA_PEM!,
    toPrincipal: (cert) => ({ sub: cert.subject.CN }),
  }),
);

app.use(fetchGuard({ allow: ["https://api.stripe.com"] }));

What the talk owns that no framework can

  • Understanding why a standard works. A framework can pick argon2id for you. It cannot make you understand what a salt is for, and the day you need to debug something, that understanding is the whole game. Watch the talk for that part.
  • Threat modeling your specific domain. No default knows that your admin route leaks tenant data across customers. That is human work.
  • Caring. The framework removes the easy excuses, but it cannot make you read the CVE. Spilca's whole pitch is that security is a literacy, not a library, and he is correct.

The takeaway

Watch the talk for the literacy: the why behind encoding versus hashing versus encryption, why reinventing crypto is a losing bet, and why PKI is back on your plate whether you like it or not. Then let a secure-by-default framework carry the parts that are pure muscle memory, so the only security decisions left on your desk are the ones that actually need a human.

That is the split I have landed on after ten years of learning these lessons the expensive way. The book and the talk make you literate. The framework keeps you from typing the obvious mistake at 1am. You want both.

Related reading on this blog: Vibe Coding Security, CSRF in 2026, CSP Nonces and Trusted Types, Secure by Default.