Does User-Typed Text Render as Text? A Proposed Output-Encoding Posture Axis for AI App Builder Output (September 2026)
A proposed benchmark axis for what a generated app does with a string a person typed when it puts that string back on a page. MDN documents that injected script elements do not execute, so the folk test returns a clean pass on a build that is not safe. Both failure directions are scored: markup that runs, and a surname that renders as punctuation it never contained.
Updated on September 22, 2026
On this page
Quick Answer
Output-encoding posture is a proposed BuilderProof benchmark axis, drafted September 22, 2026, that measures what an AI app builder's generated application does with a string a person typed when it puts that string back on a page. It is scored from the untouched export and a fixed probe battery: which sink each stored value reaches, whether the encoding applied matches the context of that sink, and whether a legitimate name survives the round trip unchanged. This page is a pre-registration rather than a report. It publishes a rubric and a protocol before any builder is measured against them, and it assigns no score to any vendor.
The problem this axis exists for
Ask an AI app builder for anything with a text field and it will give you one. A profile with a display name, a bio, a comment box, a product description, a support ticket. The field saves, the value comes back on the next screen, and the feature is finished.
Every value in that application, at the moment you are looking at it, was typed by you. You typed your own name. You typed "test". You pasted a paragraph of filler. None of those strings contains a character that means anything to a parser, so the question of what the application does with a character that does mean something to a parser is never asked, by anyone, at any point in the build.
There are two ways that question comes back, and they pull in opposite directions. In one, a value that should have been shown as text is instead read as markup. In the other, a value that should have been shown as text is escaped so enthusiastically, or escaped so early, that a person called O'Hara sees punctuation that is not punctuation. Both are invisible in a demo where the author is also the only user. Only one of them is a security problem, which is part of why the other one is so common.
What "output-encoding posture" means here
We scope this narrowly, because the neighbouring ground is already occupied and we do not want to re-measure it.
This axis is about the sink: the specific place a stored value is written into a document, and whether the transformation applied on the way in matches what the parser on the other side will do with it. It is read from the export and from a live probe battery, both scripted, so both reproduce.
It is not a vulnerability assessment. We do not attempt exploitation, we do not report findings to vendors, and a build that scores well here can still be compromised through a defect no encoding addresses. What we measure is whether a decision was made, and whether the decision was made per context.
Why this is not already covered
Three of our existing axes sit close to this one. Each stops short of it, and in two cases they say so themselves.
Against input validation and data integrity
Our input-validation axis scores what the server accepts. Its payload battery already includes, in its own words, "a string carrying SQL and HTML metacharacters", and it scores a pass when the endpoint answers that request with a 400 or a 422.
This axis scores what the application emits, and on exactly that string the two rubrics reward opposite actions. A build that rejects every apostrophe at the boundary scores well on validation and is broken for everyone whose surname contains one. A build that correctly accepts the apostrophe, stores it, and then renders it as a numeric character reference has passed validation and failed here.
Stated both ways, because that is the test. An application can parse every request body through a shared schema, parameterise every query, and still hand a stored value straight to a raw-markup sink on the way out. And an application can encode flawlessly at every sink while accepting a two hundred thousand character bio that no validation rule ever bounded.
The resolution is classification rather than choice, and the source settles it. OWASP's guidance on validation defines allowlist validation as being about structure and business meaning, and its guidance on display is a separate document with a separate answer. A metacharacter is not malformed input. It is a legitimate value that needs a different treatment at a different moment.
Against security headers and response hardening
Our security-headers axis scores what the browser is told to do. It also declines this territory in writing. Its own scope section states that it "is not a vulnerability assessment", that "it does not attempt injection", and that "a build that scores well here can still be trivially compromised through a defect no header addresses".
The standards body agrees, and more bluntly. The OWASP Cross Site Scripting Prevention Cheat Sheet lists Content Security Policy under "Other Controls" rather than under its primary recommendations, and says of it: "It's easy to make mistakes with the implementation so it should not be your primary defense mechanism. Use a CSP as an additional layer of defense." The same document devotes an entire section to the anti-pattern it calls "Sole Reliance on Content-Security-Policy (CSP) Headers".
So the two axes do not overlap, and there is a real tension between them worth recording rather than smoothing over. The strongest remedy available on this axis is delivered by a directive on that axis. OWASP describes enabling Trusted Types by sending a policy directive that causes the unsafe sinks "to reject plain strings, forcing all assignments to go through a vetted policy", and calls it "one of the few controls that eliminates entire classes of DOM XSS rather than mitigating them". A build can therefore reach the top posture here only by changing a header that the other axis scores. Neither rubric can see that on its own, and we would rather say so than pretend the boundary is clean.
Against data export and handoff
Our data-export axis covers a string whose destination is a file somebody else opens. This axis covers a string whose destination is a document the application renders itself. Different parser, different escaping rules, different observer.
They compose rather than compete, and the composition is the interesting part. A value that was encoded on the way into storage is stored wrong, and every consumer that is not a browser inherits the damage. The export carries the entity. So does the outbound email, the JSON endpoint, and the search index. That is signal 4 below, and it is the reason the axis scores where encoding happens and not merely whether it happened.
Four documented facts that make this measurable
Everything below is published by the people who maintain the thing being described.
1. The framework default covers one context, and the contexts are not interchangeable
Modern frameworks escape by default, and OWASP says so approvingly: applications built with them "have fewer XSS bugs, because these frameworks steer developers towards good security practices and help mitigate XSS by using templating, auto-escaping, and more". The same paragraph then names the gaps, and the list is short and specific. It includes "React's dangerouslySetInnerHTML without sanitizing the HTML" and, separately, that "React cannot handle javascript: or data: URLs without specialized validation".
The reason one default cannot cover everything is stated plainly a few paragraphs later: "There are many different output encoding methods because browsers parse HTML, JS, URLs, and CSS differently. Using the wrong encoding method may introduce weaknesses or harm the functionality of your application."
Note the second half of that sentence. The wrong encoding does not only fail to protect. It also breaks the page. Both failure directions are in the same clause, from the same source.
The cheat sheet then enumerates the contexts and gives each a different rule. Text between tags takes HTML entity encoding of five characters: the ampersand, the less-than sign, the greater-than sign, the double quote and the apostrophe. An attribute value takes a far more aggressive rule, encoding every character outside the alphanumerics in hexadecimal entity form, and OWASP is emphatic that the attribute must be quoted, because "Quoting makes it difficult to change the context a variable operates in, which helps prevent XSS." A URL parameter takes percent encoding, "only encode parameter values, not the entire URL or path fragments of a URL". A value inside inline script takes a Unicode escape format. A value inside a CSS property takes yet another.
And some contexts take nothing at all, because nothing is sufficient. OWASP heads that section "Dangerous Contexts" and opens it with four words that a rubric can be built on: "Output encoding is not perfect. It will not always prevent XSS." Inside a script element, inside a comment, inside a style block, and anywhere a variable would decide an attribute name or a tag name, the instruction is not to encode harder but to stop: "Don't place variables into dangerous contexts as even with output encoding, it will not prevent an XSS attack fully."
This is why the axis scores context-matching and not escaping. A single global escape function is an answer to a question nobody asked.
2. The one adversarial test everybody knows is specified to do nothing
There is a single probe that almost every developer knows: type a script element into the field and see whether an alert appears. It is the folk test for this entire class of defect.
MDN's reference for the most common raw-markup sink documents, in its security section, that the probe is specified not to work. The property "is probably the most common vector for cross-site scripting (XSS) attacks, where potentially unsafe strings provided by a user are injected into the DOM without first being sanitized", and then: "While the property does prevent script elements from executing when they are injected, it is susceptible to many other ways that attackers can craft HTML to run malicious JavaScript."
The page supplies the counterexample immediately. Assigning a string containing an image element whose src attribute is the single character x, and which carries an onerror handler, runs the handler, because the src is not a valid image URL. MDN's comment on the line is three words: it shows the alert.
That is the sharpest looks-correct control we have written into any protocol on this site. The folk test returns a clean pass on a build that is not safe, and it returns that pass because the platform specified it to. A team that runs it once and moves on has not been careless. They have run the test that the documentation says will not fire.
Step 3 of the protocol below is that probe, recorded explicitly as an observation that carries no information, so nobody reads it as evidence later.
3. The safe remedy breaks the feature, and the platform's own sanitizer is not yet universally available
When a person is supposed to be able to author formatting, encoding is the wrong answer and OWASP says so: "Output encoding in this case will prevent XSS, but it will break the intended functionality of the application. The styling will not be rendered." The recommendation for that case is sanitization, and the cheat sheet names a specific library.
It attaches two conditions that matter for scoring. First: "If you sanitize content and then modify it afterwards, you can easily void your security efforts." Second: "You must regularly patch DOMPurify or other HTML Sanitization libraries that you use. Browsers change functionality and bypasses are being discovered regularly."
The platform has been building a first-party answer to the same problem. MDN describes the sanitizing parser method as providing "an XSS-safe method to parse and sanitize a string of HTML and insert it into the DOM as a subtree of the element", which "removes any elements and attributes that are considered XSS-unsafe, even if allowed by a passed sanitizer".
Its availability banner reads: "Limited availability. This feature is not Baseline because it does not work in some of the most widely-used browsers."
That is a direct composition with the axis we proposed yesterday. Adopting the built-in remedy requires knowing which engines the build is actually expected to run in, which is precisely the question our browser-support axis proposes to score. A team that has never resolved its browser floor cannot evaluate a remedy whose availability is defined in terms of that floor.
The same MDN page also corroborates OWASP's warning from the other direction, on the re-serialization hazard: even after sanitizing, "it is still not safe to serialize the HTML and re-parse it" through the raw sink. Two independent maintainers describing the same failure is worth more than either of them saying it alone.
4. Encoding in the wrong place corrupts the data, and the source gives the worked example
The clearest published account of the over-encoding direction is in OWASP's discussion of why a request interceptor is an anti-pattern. The objection is that a filter sitting in front of the whole application cannot know the context: "how does a servlet filter know that an input query parameter is going to be rendered in an HTML context (i.e., between HTML tags) rather than in a JavaScript context such as within a script tag or used with a JavaScript event handler attribute? It doesn't."
Then the consequence, with a name in it: "suppose in the previous example, a developer has done proper output encoding for the JavaScript rendering of lastname. But if it is already been HTML output encoded too, when it is rendered, a legitimate last name like 'O'Hara' might come out rendered like 'O'Hara'."
OWASP records what happens next, and it is a governance observation rather than a technical one: "if it happens often enough, it can result in business push-back against the use of the filter and thus the business may decide on disabling the filter or a way to specify exceptions for certain pages or parameters being filtered, which in turn will weaken any XSS defense that it was providing."
A correctness defect visible to ordinary users is how a security control gets switched off. That is why signal 4 is weighted where it is, and why the probe battery includes a name that is merely awkward rather than hostile.
The same section closes the loop on where tainted data comes from. Interceptors, OWASP notes, "generally are oblivious to data in your application's responses that originate from other internal sources such as an internal REST-based web service or even an internal database", and it works the example through to a customer support screen rendering an address field that a customer poisoned. A generated application that escapes its own form input and renders a third-party API response untouched has the same shape.
What the framework default actually buys, and why it hides the rule
It is worth being precise about the default, because the default is doing most of the work and is the reason this is hard to see.
React's documentation describes its escape hatch as an object "with a raw HTML string or TrustedHTML value inside", which "Overrides the innerHTML property of the DOM node and displays the passed HTML inside", and then: "This should be used with extreme caution! If the HTML inside isn't trusted (for example, if it's based on user data), you risk introducing an XSS vulnerability."
The longer treatment is more pointed still, because of what it is demonstrating. The worked example is a markdown preview: user-authored markdown, converted to markup, rendered. React's own security counterexample seeds the stored value with a comment reading that the content is imagined to be stored in the database, and the payload it uses is the same image-with-onerror shape MDN documents. React labels the line with a red circle and the words security hole.
Two things follow that a rubric can use.
The first is that the framework named its own escape hatch with a warning inside the identifier. You cannot reach it by accident. Every use of it is a deliberate act, and a generated application either contains one or does not, which makes it a clean static signal.
The second is subtler and is the real reason this axis exists. The default here is safe, and that is exactly why nobody learns the rule. A team ships a dozen screens rendering values as text between tags, and every one of them is correct, and none of them was a decision. The build is at posture level 0 and looks like level 2. The first context the default does not cover is then met with no mental model at all, and that context arrives on the day somebody asks for a rich-text bio, a link field, or a markdown-formatted description.
We have named a recurring pattern on this site where a default nobody chose decides a correctness property. This is the inverted case, and it is the eighth we have recorded. Usually the unchosen default is wrong. Here it is right, and being right is what prevents anyone from noticing that a choice exists.
The proposed rubric
Seven signals, summing to one hundred. These weights are a starting point for community revision, not a settled formula.
Scroll to see more
| Signal | Weight | What a failing case looks like |
|---|---|---|
| A value that arrived from outside renders as text in every context the build uses | 22 | A display name is written into a raw-markup sink, so a value containing an image element with an error handler runs it; or a bio reaches the document through a markdown renderer that passes embedded markup straight through |
| Raw-markup sinks, where present, are paired with a sanitizer at the sink | 20 | The escape hatch is used on a stored string with nothing between them; or a sanitizer runs and its output is then re-serialized and re-parsed, which both maintainers document as unsafe |
| Encoding is chosen for the context rather than applied once globally | 16 | One escape helper is used for body text, attribute values and URLs alike, so an unquoted attribute remains escapable and a percent-encoding context receives entity encoding |
| Values are stored as typed and encoded at render, not encoded at write | 14 | The apostrophe in a surname is stored as an entity, so the export, the outbound email and the API all carry punctuation that is not punctuation, and a second encoding pass at render doubles it |
| URL-valued attributes are scheme-checked, not merely escaped | 12 | A profile link field accepts a javascript scheme and is emitted into an href; escaping does not help, because the scheme is the payload rather than the delimiter |
| Data that did not arrive through a form is treated as untrusted too | 9 | Form input is escaped and a value from a third-party API, a seeded row or an internal service is rendered raw, because only the request body was ever considered tainted |
| The unsafe sinks are constrained rather than merely unused | 7 | Nothing prevents the next generated component from reaching for the escape hatch; the posture rests on nobody having done it yet |
Three weighting decisions worth arguing about now
Why the first signal carries the most. It is the only row whose failure is reachable by a stranger rather than by the operator, and the only one where the consequence is executed code rather than wrong text. It is also the row the folk test fails to detect, which means its real-world detection rate is lower than its severity suggests.
Why storage placement is weighted above scheme checking. Encoding at write time is the failure most likely to be shipped by a competent team trying to do the right thing, and it is the one that degrades every non-browser consumer at once rather than one field on one screen. OWASP's own account of it ends with the control being switched off, which is a worse outcome than the defect it was introduced to fix.
Why sink constraint is last and still on the table. Trusted Types and the sanitizing parser genuinely eliminate the class rather than mitigating it, and on that basis the row deserves more. It is weighted at 7 because both remedies are currently conditional: one is delivered by a header the build may not send, and the other is documented as not yet working in some of the most widely used browsers. A signal a team cannot act on everywhere should not dominate a score. We expect this weight to be the first thing revised.
Four postures
Level 0, Inherited. Nothing was decided. Values reach the document through whatever the framework does by default. Where that default covers the context the build is safe, and it is safe by inheritance rather than by intent. The first sink outside the default is unprotected, and no one involved can say which sinks those are.
Level 1, Escaped. Escaping is deliberate and uniform. One helper is applied everywhere. This closes the common body-text case and introduces the over-encoding case, because the same transformation now runs in contexts that needed a different one or none at all. Level 1 is where a legitimate surname starts rendering as punctuation that is not punctuation.
Level 2, Context-matched. Each sink is encoded for the parser behind it. Raw-markup sinks, where the feature requires them, pass through a sanitizer at the sink. Values are stored as they were typed. URL-valued attributes are checked for scheme, not merely escaped.
Level 3, Sink-constrained. The unsafe sinks are unavailable rather than unused. A vetted policy or a sanitizing parser stands between any string and the document, so a component generated next week cannot reintroduce the defect without failing loudly.
One observation about the ladder, because the steps are not equivalent. The step from 0 to 1 is the only one a demonstration can show, since it is the only one that changes what happens to a hostile string. The step from 1 to 2 is the only one that changes what happens to an honest string, which means it is the only step an ordinary user can detect. And the step from 2 to 3 is the only one that produces a guarantee rather than a state, because it constrains what the code can do later rather than describing what it does now.
The reproduction protocol
Ten steps. Scripted end to end, so two runs on the same export agree.
- Generate a reference application from the fixed prompt suite, one generation per builder, no manual edits. The reference brief includes at least one field a person types and two surfaces that display it, so there is more than one sink to enumerate.
- Enumerate the sinks. From the export, list every place a stored value reaches the document, and classify each as body text, quoted attribute, unquoted attribute, URL-valued attribute, inline style, inline script, or raw-markup sink. Record the count. A build with one sink and a build with nine are not comparable without it.
- Control step, and it is expected to pass. Submit a script element as the field value and view every sink. Record the result and mark it as carrying no information, because the reference documentation states that this specific probe does not execute through the most common raw sink. A pass here is not evidence.
- Submit the documented image-with-error-handler payload and view every enumerated sink. Record, per sink, whether the value appears as text, as rendered markup, or as nothing at all.
- The honest-value probe. Submit a name containing an apostrophe and an ampersand, such as O'Hara and Sons. Record three things separately: what the page displays, what the database row contains, and what the application's own export or API returns for the same field. Divergence between the three is the finding.
- Submit a value into every URL-valued field the build exposes, using a javascript scheme, and record whether the attribute is emitted, rewritten or rejected.
- Read the same stored value back through a second surface the build provides that is not a browser page. Any encoding visible there was applied at the wrong moment.
- Statically classify every raw-markup sink found at step 2: is a sanitizer called, is the sanitized output re-serialized afterwards, and is the sanitizer a dependency with a version and an update path.
- Provenance step. Seed a value through a path that is not the form, by writing directly to the database or by pointing a build at a stub third-party response, then render it. This separates a build that escapes request bodies from a build that encodes at the sink.
- Score each signal with the submitted payload, the rendered output and the stored bytes attached, so the arithmetic is auditable rather than asserted. The method follows our reproducibility-first methodology.
The named trap: the own-content illusion
Every string in a newly generated application was typed by the person who generated it.
That is the whole trap, and it is worth saying slowly, because it does not feel like a limitation while you are inside it. You are the author, the operator and the only user. You type your own name into the name field. You type "test" into the title. You paste filler into the description. You are, without ever deciding to, supplying an input distribution that contains no delimiters, no markup, and frequently no punctuation at all. The application handles every value you have ever given it correctly, and it has never been given a value that could have been handled incorrectly.
What makes this one worse than its relatives is that the escape route is booby-trapped. Most traps in this series are invisible because nobody thinks to look. Here people do think to look, there is a well-known probe, and the documentation says the probe does not fire. A developer who tests, gets a clean result, and ships has followed the one piece of adversarial practice the folk knowledge provides.
This is the fifteenth trap in this series to share a one-actor structure, where every observation available at test time is produced by the same actor who built the thing. It belongs with the seeded corpus, where the person searching wrote the documents, and with the preview pane, where the only engine consulted is the engine doing the building. It differs from both in one respect that is worth recording. The seeded corpus is too small; the preview pane is a single sample. The own-content illusion is neither. It is a full, realistic, correctly-shaped body of content that happens to be drawn from a population of one, and it will keep passing indefinitely, because the second person to type into the box is by definition not present while the box is being built.
What this axis is not
It is not a vulnerability assessment. We do not attempt exploitation, we do not publish payloads beyond the two the reference documentation already publishes, and no claim here means any application is or is not compromisable.
It is not a judgement about the frameworks whose documentation is quoted. Every warning cited above was written by the project that ships the feature, and several were written as warnings on purpose. React named its escape hatch so that it could not be used inattentively. MDN documents the limits of its own most-used property. Documenting a limitation clearly is the opposite of a defect.
It is not a claim about any builder's current standing. No first-party measurement has been run. If one is, the protocol above, control step included, will be published alongside it before any placement is stated.
Limitations and open questions
- Single-generation variance. One export is one sample, and a builder that varies between generations needs a multi-run design before any score is fair.
- Prompt sensitivity. A brief that asks for sanitized user content would measure instruction-following rather than default posture. The reference brief describes the application, not its defences.
- Sink count is not held constant. A build that renders one field in one place has fewer opportunities to fail than a build that renders the same field in six, and scoring the second more harshly would reward doing less. Step 2 records the count, but the rubric does not yet normalise by it, and we think it should. This is the open question we would most like an argument about.
- Markdown rendering is the common case and we have not surveyed it. The libraries a generated application is likely to reach for differ in whether embedded markup is passed through by default, and that single default decides signal 1 for an entire class of build. A survey of those defaults belongs in this axis and is not in it yet.
- One signal is time-dependent. Sanitizer maintenance is a property of a date, not of an export. A score on that row is only true on the day it was read.
- Server-rendered and client-rendered sinks were not separated. The same value can be encoded correctly on the server and then written into the document again on the client through a different path. The protocol as written would record the first and might miss the second.
- Scores may cluster. Framework defaults do most of the work, so we expect the body-text case to be handled nearly everywhere. The axis discriminates at the contexts the default does not reach, and if that turns out to be a narrow band, the weights are wrong rather than the axis.
What we are not claiming yet
No scores today. This page proposes the axis, the rubric, the postures and the method, and ranks nobody. Publishing the rubric before the results is deliberate, so that the weights, the probe battery and the reproduction steps can be argued with before any builder is measured against them. BuilderProof axes are community-editable by design, and the proposal stage is where that editing does the most good.
If your run of the probe battery disagrees with what we eventually publish, that is the most useful thing you can send us.
References
- OWASP Cheat Sheet Series, "Cross Site Scripting Prevention Cheat Sheet". Framework escape hatches, the per-context encoding rules, the dangerous-contexts section, HTML sanitization and its two conditions, safe sinks, Trusted Types, and the anti-pattern sections on sole reliance on Content Security Policy and on request interceptors including the O'Hara double-encoding example. https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html (read 22 September 2026)
- MDN Web Docs, "Element: innerHTML property". The security-considerations section, the statement that script elements injected through the property do not execute, and the image-with-error-handler counterexample. https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML (read 22 September 2026)
- MDN Web Docs, "Element: setHTML() method". The XSS-safe parse-and-sanitize description, the always-removed element list, the limited-availability banner, and the re-serialization hazard. https://developer.mozilla.org/en-US/docs/Web/API/Element/setHTML (read 22 September 2026)
- React, "Common components (e.g. div)". The escape-hatch prop description and its caution, the markdown-preview worked example, and the security-hole counterexample. https://react.dev/reference/react-dom/components/common (read 22 September 2026)
- BuilderProof, "How we benchmark AI app builders: methodology v1". https://www.builderproof.org/benchmarks/how-we-benchmark-ai-app-builders-methodology-v1
Written by
BuilderProof editorial teamCite this benchmark
BuilderProof editorial team. "Does User-Typed Text Render as Text? A Proposed Output-Encoding Posture Axis for AI App Builder Output (September 2026)". BuilderProof, September 2026. https://www.builderproof.org/benchmarks/does-user-typed-text-render-as-text-output-encoding-axis-september-2026.
@misc{builderproof-does-user-typed-text-render-as-text-output-encoding-axis-september-2026,
title = {{Does User-Typed Text Render as Text? A Proposed Output-Encoding Posture Axis for AI App Builder Output (September 2026)}},
author = {{BuilderProof editorial team}},
year = {2026},
month = {sep},
howpublished = {\url{https://www.builderproof.org/benchmarks/does-user-typed-text-render-as-text-output-encoding-axis-september-2026}},
note = {BuilderProof, builderproof.org}
}Frequently asked questions
What does the output-encoding posture axis measure?
It is a proposed BuilderProof benchmark axis, drafted 22 September 2026, that scores what the application an AI app builder generates does with a string a person typed when it writes that string back into a document. It is read from the untouched export plus a fixed probe battery: which sink each stored value reaches, whether the encoding applied matches the parser behind that sink, and whether a legitimate name survives the round trip unchanged. It is a pre-registration. No builder is scored on this page.
Is this not already covered by the input-validation axis?
No, and on one specific input the two rubrics reward opposite actions. The input-validation axis scores what the server accepts, and its own payload battery treats a string carrying HTML metacharacters as something an endpoint should reject with a 400 or 422. This axis scores what the application emits, and a metacharacter is a legitimate value rather than malformed input. A build that rejects every apostrophe passes there and is broken for everyone called O'Hara; a build that accepts it and then renders it as a numeric character reference passes there and fails here.
Why is testing with a script element not enough?
Because the platform specifies that probe not to fire. MDN's reference for the most common raw-markup sink states that while the property does prevent script elements from executing when they are injected, it is susceptible to many other ways attackers can craft HTML to run JavaScript, and supplies an image element with an error handler as the counterexample. So the single adversarial test most developers know returns a clean pass on a build that is not safe. The protocol records it as step 3 and marks it explicitly as carrying no information.
Does a Content Security Policy not solve this?
OWASP says it should not be relied on to. Its cross-site scripting guidance lists Content Security Policy under other controls, states that it is easy to make mistakes with the implementation so it should not be your primary defense mechanism, and devotes a section to the anti-pattern of sole reliance on it. Our own security-headers axis draws the same line from the other side, stating that it does not attempt injection and that a build scoring well there can still be compromised through a defect no header addresses.
Related benchmarks
Input-Validation and Data-Integrity Posture: A Proposed Axis for Whether AI App Builders Validate Untrusted Input at the Boundary (August 2026)
A candidate BuilderProof benchmark axis that scores whether the code AI app builders emit validates untrusted input at the server boundary, or trusts whatever the client sends. Rubric, four posture levels, an adversarial-payload reproduction protocol, and an open call for comment.
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.
Does It Run in the Browser They Actually Have? A Proposed Browser-Support Axis for AI App Builder Output (September 2026)
A proposed benchmark axis for which browser engines a generated app can actually run in. Baseline defines widely available as 30 months of interoperability and names what it cannot see. Next.js floors at Firefox 111, Vite resolves its default to Firefox 114, and the preview pane reports a pass either way. Seven weighted signals, four postures, a ten-step protocol, and no scores.