Methodology
BuilderProof Editorial Team16 min read52 views

What security headers does your generated app actually send? A response-hardening axis for AI app builders (September 2026)

A proposed BuilderProof axis measuring whether the applications AI app builders generate actually deliver security response headers, and whether the policies they do ship bind where they are delivered. Seven weighted signals, four postures, a ten-step protocol with a control step. Pre-registered: no builder is scored here.

Blueprint line diagram: a server rack on the left sends one arrow rightward through a row of upright gate frames, two drawn in solid line and one only as a dashed amber outline that the arrow passes straight through, reaching a browser window holding a dashed amber panel askew inside it.
Blueprint line diagram: a server rack on the left sends one arrow rightward through a row of upright gate frames, two drawn in solid line and one only as a dashed amber outline that the arrow passes straight through, reaching a browser window holding a dashed amber panel askew inside it.
On this page

Quick answer. Nobody has measured whether AI app builders emit security response headers in the applications they generate, and this page does not measure it either. What follows is a pre-registration: a proposed axis, the weights we would use, the failing case for each signal, and the exact protocol we would run. No builder is scored here and no builder is named in a posture. The reason to publish the rubric before the results is that a rubric written after you have seen the numbers is a rubric you can no longer defend.

The short version of the finding that motivated it: the two escape routes available to a code generator are both documented as defective, by the people who wrote the specifications and the framework. The cheap route silently drops the protections that matter most, and the strict route is documented as costing static rendering, incremental regeneration and CDN caching. That is a measurable situation, which is what makes it an axis rather than an opinion.

Why this is not already covered

Three axes in this series come close enough that the boundary has to be drawn explicitly, in both directions, or the axis is not worth proposing.

Against HTTP caching and revalidation correctness

Our HTTP caching and revalidation axis also scores HTTP response header declarations, and its heaviest signal is private-response scoping. The boundary is not one we have to assert, because RFC 9111 draws it itself. In the note attached to the private directive in section 5.2.2.7, the specification says:

RFC 9111, section 5.2.2.7, verbatim: "This usage of the word 'private' only controls where the response can be stored; it cannot ensure the privacy of the message content."

That is the whole carve-out in one sentence, written by the document that defines the directive. The caching axis scores who may store and replay a response. This axis scores what a browser may execute, embed or leak once it holds one. Same header field family, one specification, and the specification is explicit that the first does not deliver the second.

The two move in opposite directions, and both directions are real. An application can declare private, no-store on every authenticated response, scoring at the top of the caching axis, and ship no script policy at all, so an injected script reads the same data live out of the DOM with no cache involved anywhere. The reverse also holds: an application can ship a nonce-based policy that blocks every injected script and still return a personal dashboard with a shareable one hour lifetime that an intermediary hands to a second user, which no security header prevents.

There is also a documented tension between the two axes, not merely a boundary, and it is discussed in the cost section below.

Against auth and access control

Our auth and access-control posture axis scores whether the generated application has an authentication system and whether the server enforces who may read what. This axis scores what the response instructs the browser to enforce. Server-side enforcement and browser-side instruction are different mechanisms with different failure modes.

Opposite directions, stated both ways. An application can scaffold sign-in, sessions and per-row isolation correctly, so the server never returns a row the caller may not see, and still set its session cookie without HttpOnly, so one injected script reads that cookie and replays the session from anywhere. And an application can set every cookie flag correctly and pin every header, while its data layer has no per-tenant policy at all, so an authenticated user reads every other tenant's rows through a perfectly hardened response.

Against input validation and data integrity

Our input-validation axis scores what the server accepts. This axis scores what the browser is told to do with what the server returns. The distinction matters because most script injection in a generated application does not arrive through a validated form field. It arrives through a value the application fetched from somewhere it trusts.

Both directions again. An application can validate every request body against a schema and parameterise every query, and still have no script policy, so a value that arrived from a third-party API and was rendered into a page executes. And an application can ship a strict script policy and still mass-assign a request body onto a model, which no response header can prevent.

Three documented facts that make this measurable

An axis is only worth proposing if the failing cases are checkable against published requirements rather than against taste. These three are.

W3C

1. The most restrictive-looking policy still allows framing

Content Security Policy Level 3 defines frame-ancestors, the directive that controls which origins may embed a document. It also states that this directive does not inherit the fallback behaviour every other fetch directive has. In the specification's own note:

CSP Level 3, on frame-ancestors, verbatim: "The frame-ancestors directive's syntax is similar to a source list, but frame-ancestors will not fall back to the default-src directive's value if one is specified. That is, a policy that declares default-src 'none' will still allow the resource to be embedded by anyone."

default-src 'none' is the most locked-down policy a person is likely to write, and it is the policy a generator is most likely to produce if asked for a strict one. It leaves clickjacking completely unaddressed. That is a checkable failing case, and it is checkable without knowing anything about the application.

MDN

2. The delivery mechanism decides which directives bind

A code generator that emits application source but does not own the server configuration has one obvious way to ship a policy: put it in the document head. Both anti-framing mechanisms are documented as unavailable by that route.

The CSP specification states that "the frame-ancestors directive MUST be ignored when contained in a policy declared via a meta element." And MDN's reference for the older header carries a warning in the same direction: "Setting X-Frame-Options inside the meta element (e.g., meta http-equiv='X-Frame-Options' content='deny') has no effect. X-Frame-Options is only enforced via HTTP headers."

So the single delivery route available to a pure content generator cannot deliver either form of embedding protection. The policy appears in the source, a reviewer reading the source sees a policy, and the browser enforces none of that part of it. This is why the rubric below scores delivery separately from content, rather than folding it into the policy signals.

Two smaller facts belong here. The same specification records that frame-ancestors overrides the older header where both are present and enforcing, so an application that sets both is not doubling up, it is relying on exactly one of them. And MDN records that the ALLOW-FROM form of the older header is obsolete and that modern browsers "will ignore the header completely", which is a way for a policy to be present, plausible and inert.

Vercel

3. The strict fix has a cost the framework documents

The reason to suspect this axis will separate builders is not that adding headers is hard. It is that the correct form of the fix is documented as expensive, in the framework's own words.

A script policy that omits unsafe-inline needs some way to permit the inline scripts a framework legitimately emits. The nonce approach is the common answer, and Next.js documents what it costs. Its guidance states that "you must use dynamic rendering to add nonces", and spells out the consequences: "Static optimization and Incremental Static Regeneration (ISR) are disabled", "Pages cannot be cached by CDNs without additional configuration", and Partial Prerendering "is incompatible with nonce-based CSP since static shell scripts won't have access to the nonce". Under performance implications it lists slower initial page loads, increased server load, no CDN caching and higher hosting costs.

This is the tension with our caching axis, stated plainly: the strict form of this axis's second signal actively degrades the caching axis. They are not independent, and a scoring system that pretends they are will reward a build for something it was punished for one page earlier. The honest handling is to score both and report the conflict, which is what a pre-registration is for.

It is worth recording that the framework has since documented an alternative. Hash-based policies using Subresource Integrity are described as allowing you to "maintain static generation while still having a strict CSP", with hashes generated at build time. The documentation labels that support experimental and App Router only. An axis should notice which of the two routes a build took, and should not penalise a build for declining an experimental one.

IETF

A note on transport pinning

Strict Transport Security is the one header on this list whose behaviour is genuinely time-dependent, and the axis should not overstate what it buys. RFC 6797 makes max-age a required directive, and states that "an HSTS Host MUST NOT include the STS header field in HTTP responses conveyed over non-secure transport." The policy is therefore learned only from a response the browser already received securely. It cannot protect the very first request to a host it has never seen. That is a real limit, it is why the signal is weighted below the script and framing signals, and it is not a reason to omit the header.

The proposed rubric

Seven signals, one hundred points. Each row states the failing case rather than the ideal, because a failing case is something you can check against your own build in an afternoon.

Scroll to see more

SignalWeightWhat a failing case looks like
Session cookie attributes22The session cookie is set without HttpOnly, so any injected script can read it and the session becomes portable; or without Secure, so it can ride a single plaintext request; or without an explicit SameSite, so its cross-site behaviour is whatever the user's browser happens to default to
Script-execution policy20No Content-Security-Policy is sent at all, or one is sent whose script-src contains 'unsafe-inline', so any value that reaches the document can execute and the policy constrains nothing that an attacker would actually attempt
Embedding policy16Neither frame-ancestors nor X-Frame-Options is delivered in an enforcing position, so any third-party page may embed the application in a frame; the documented default-src 'none' case belongs here, not under the script signal
Header delivery and route coverage13The policy is present in the document head rather than as a response header, so the directives that govern embedding are ignored; or headers are applied to page routes while API routes and error responses return bare
Transport pinning12No Strict-Transport-Security, so the browser holds no policy of its own and a request that begins over plaintext is upgraded only if the server happens to redirect it
Content-type and referrer hygiene10No X-Content-Type-Options: nosniff, so a stored upload can be re-interpreted as a script type; and no Referrer-Policy, so full URLs including anything embedded in a path or query leave the origin on every outbound navigation
Policy coherence and reportability7The policy contains directives the specification documents as inert in the context they were delivered in, or source expressions that silently cancel each other, and there is no reporting endpoint that would ever reveal it

Three weighting decisions worth arguing about now

Why cookie attributes carry the most. They are the only row whose failure converts a single successful injection into a durable, transferable account takeover. Every other row on this list makes an attack easier or harder; this one decides whether the attacker keeps the session afterwards. It is also the cheapest row to get right, which is what makes a failure here informative about the generator rather than about the difficulty of the problem.

Why delivery is a signal rather than a precondition. It would be tidier to treat delivery as a gate and score only applications that clear it. That would be wrong, because the interesting failure is precisely a policy that exists and does not bind. Scoring delivery separately is what lets the rubric distinguish a build that shipped nothing from a build that shipped something inert, and those are different defects with different remedies.

Why coherence is small. An incoherent policy is a real defect, but it is usually a symptom of one of the rows above rather than an independent risk, and weighting it heavily would let a build with no policy at all avoid the penalty by having nothing to be incoherent about.

Four postures

Postures describe a build's structural position, not its score. They exist so that a low number and a high number can be told apart by kind rather than by degree.

Level 0, Absent. No security response headers beyond whatever the hosting platform adds on its own. Cookie attributes are framework defaults. Nothing in the generated source expresses a policy.

Level 1, Decorative. A policy exists in the repository and does not bind where it matters. This is the level the documented facts above make reachable by accident: a policy in a document head, a policy whose script-src permits inline script, a default-src 'none' with no frame-ancestors, an obsolete header form. A reviewer reading the source sees security work. A browser sees very little.

Level 2, Enforced. Real response headers on every route the application serves, including API and error routes. Cookie attributes set explicitly rather than inherited. A script policy without unsafe-inline, delivered by a mechanism where the embedding directive is honoured.

Level 3, Verified. Everything in Level 2, plus evidence that it stays true. A reporting endpoint is configured and receives violations, and the build asserts its own headers somewhere a regression would fail rather than pass quietly. The distinction between Level 2 and Level 3 is not strictness, it is whether anything would notice if the policy stopped being delivered.

The measurement protocol

Ten steps. The protocol is written so that a reader can run it against their own build without our harness, which is the only way a proposed axis can be argued with before it ships.

  1. Generate an application from a prompt that requires authentication, so a session cookie exists to inspect. Deploy it to the target the builder recommends, because these headers are a property of the deployment, not of the repository.
  2. Request the application root and capture the full response headers, not the body. Record every header present rather than checking for a list you expect, so headers you did not think to ask about are still recorded.
  3. Sign in and capture the Set-Cookie responses. Record every attribute on the session cookie, and record the absence of attributes explicitly rather than by omission.
  4. Repeat step 2 for an authenticated page route, an API route and a deliberately failing route. Coverage gaps concentrate at the routes nobody demonstrates.
  5. Search the served HTML for a policy declared in the document head. If one is present, record which of its directives the specification treats as inert in that position, and score those as absent.
  6. Parse the script policy and record whether unsafe-inline is present, whether a nonce or hash source is present, and whether the policy would permit inline script by omitting a script-src and a default-src entirely.
  7. Record whether frame-ancestors is present as a distinct directive rather than inferred from default-src. This is the step that catches the documented fallback gap, and it is the step most likely to change a score.
  8. This is the control. Request the same routes from a deployment of the same generated application with any hand-added configuration removed, so the measurement separates what the builder emitted from what the operator added afterwards. Without this step the axis measures the operator.
  9. Re-run steps 2 and 4 after a second deployment. A header set that appears once and not again is a configuration that did not survive the build, which is a different finding from a header set that was never written.
  10. Record whether adding a strict policy would have forced the build off static rendering, using the framework's own documented behaviour. A build that chose caching over a nonce made a documented trade, and the axis should record the trade rather than only the outcome.

Steps 8 and 9 are the ones most likely to be skipped and the ones that make the rest meaningful. Step 8 separates the builder from the person using it. Step 9 separates a policy from an accident.

The named trap: the declared-policy illusion

Each axis in this series names the illusion that hides its defect. Tests that pass without asserting are the green-check illusion. A form that validates in the browser and nowhere else is the green-form illusion. A page that renders no state because its request never resolved is the quiet-channel illusion. A function that was called but never awaited is the fire-and-forget illusion.

The trap here is the declared-policy illusion, and it is the reason this axis needs a delivery signal at all.

Writing a policy feels like enforcing one. It does not feel provisional. A policy is a concrete artefact: it sits in a configuration file, it is long, it is full of directives, it survives review, and it looks exactly as correct when it binds as when it does not. All three of the documented facts above produce a policy that reads correctly in the source and enforces less than the source implies, and none of them announces itself. A policy in a document head does not warn you that its embedding directive was dropped. A default-src 'none' does not warn you that framing is still permitted. An unsafe-inline does not warn you that the script directive it sits in has been reduced to decoration.

The failure mode of this axis is therefore not an absent policy, which is easy to see and easy to fix. It is a present one that a reviewer, a generator and a security checklist all agree looks fine, and that a browser quietly declines to act on. The only instrument that separates the two is a request, issued against the deployed application, that reads what actually arrived.

What this axis is not

It is not a vulnerability assessment. It does not attempt injection, it does not test whether a given application is exploitable, and a build that scores well here can still be trivially compromised through a defect no header addresses.

It is not a judgement about the platforms whose documentation is quoted above. Every requirement cited is one those projects published themselves, and several are published precisely as warnings. Documenting a limitation clearly is the behaviour you want from a platform, and quoting it is not a criticism of it.

It is not a claim about any builder's current standing. No first-party measurement has been run, and the results of any run would be published with the protocol above, its control step included, before any placement is stated.

The methodology this axis would be scored under is described in how we benchmark. If a signal here is weighted wrongly, the argument is easier to make now than after the numbers exist.

References

Every source below was read in full on 6 September 2026. Quotations are verbatim.

Cite this benchmark

Plain text
BuilderProof Editorial Team. "What security headers does your generated app actually send? A response-hardening axis for AI app builders (September 2026)". BuilderProof, September 2026. https://www.builderproof.org/benchmarks/what-security-headers-does-your-generated-app-send-axis-september-2026.
BibTeX
@misc{builderproof-what-security-headers-does-your-generated-app-send-axis-september-2026,
  title  = {{What security headers does your generated app actually send? A response-hardening axis for AI app builders (September 2026)}},
  author = {{BuilderProof editorial team}},
  year   = {2026},
  month  = {sep},
  howpublished = {\url{https://www.builderproof.org/benchmarks/what-security-headers-does-your-generated-app-send-axis-september-2026}},
  note   = {BuilderProof, builderproof.org}
}

Frequently asked questions

What does this axis measure?

Whether the application an AI app builder generates delivers security response headers, and whether the policies it does ship bind in the position they are delivered. It covers session cookie attributes, script-execution policy, embedding policy, header delivery and route coverage, transport pinning, content-type and referrer hygiene, and policy coherence. It is a pre-registration: the rubric and protocol are published before any measurement, and no builder is scored on this page.

Why does a Content-Security-Policy in a meta tag not count?

Because the specification says two of its directives do not bind there. CSP Level 3 states that the frame-ancestors directive MUST be ignored when contained in a policy declared via a meta element, and MDN records that setting X-Frame-Options in a meta element has no effect because it is only enforced via HTTP headers. A policy delivered in the document head therefore cannot deliver either form of embedding protection, while still looking complete to anyone reading the source.

Does default-src 'none' block my app from being framed?

No. CSP Level 3 states that frame-ancestors does not fall back to default-src, and gives the consequence explicitly: a policy that declares default-src 'none' will still allow the resource to be embedded by anyone. Embedding protection requires frame-ancestors as a distinct directive, delivered as a real response header.

Is this the same as your HTTP caching axis?

No, and RFC 9111 draws the boundary itself. Its note on the private directive says that the word private only controls where the response can be stored and cannot ensure the privacy of the message content. The caching axis scores who may store and replay a response; this axis scores what a browser may execute, embed or leak once it holds one. The two also conflict in one documented place: a nonce-based policy requires dynamic rendering, which the framework documents as disabling static optimization, ISR and CDN caching.

What is the declared-policy illusion?

It is the trap this axis names. Writing a policy feels like enforcing one, because a policy is a concrete artefact that survives review and looks equally correct whether or not it binds. A policy in a document head, a script-src containing unsafe-inline, a default-src 'none' with no frame-ancestors and an obsolete ALLOW-FROM header all read as security work in the source while a browser acts on very little of them. Only a request issued against the deployed application separates the two.

HTTP Caching and Revalidation Correctness: A Proposed Axis for What AI App Builders Declare About Reusing Their Own Responses (September 2026)

A candidate BuilderProof axis scoring what a generated application declares about who may store its responses, for how long, keyed on what, and how staleness is detected. The HTTP caching specification's shared-cache protection is keyed on the Authorization header, which is not how most generated apps authenticate. Seven weighted signals, four postures, a nine-step protocol, no scores.

22 min read47