CamberStack’s public digest form escapes quotes in an invalid email before redisplaying it. The same field also accepts a nested parameter shape, however, and its keys become raw HTML attribute names. One crafted digest URL turns that type change into JavaScript execution in the forum’s origin.
Every case takes a real Uphack lab and walks the exact steps that find its flaw, one at a time. The lab itself is waiting at the end.
It plays like a video, except you drive it: scroll, and the panel beside the text walks the screens, the traffic and the code down to the exact line that fails.
Every screen, request and line of code is taken from the running lab, down to the line numbers. Nothing here is illustrative.
CamberStack is a technical car forum whose weekly digest collects setup threads, staff-reviewed safety notes and build logs. The digest signup is public and submits with GET, so a visitor can share a URL containing the complete form state.
The form must preserve a rejected email so the visitor can correct it. That ordinary usability feature creates the relevant output path: request data returns inside the email input.
An invalid address produces Check the digest form and leaves the submitted text in the field. Because the value appears inside an HTML input, the renderer must keep quotes and angle brackets from changing the element’s structure.
The first request places a quote, autofocus and an onfocus handler in one scalar digest[email] value. If the quote closed the input’s value attribute, the remaining text would become executable attributes.
The response encodes the quote as ". The browser therefore treats the entire payload as the value of one attribute, and focusing the field does not execute the handler.
The response demonstrates a working defense on the string path. Routine tests with malformed email text would pass because the renderer handles that shape safely.
The field name already uses bracket notation. Adding one more bracket sends digest[email][disabled], which changes the submitted email from a scalar value into a nested shape. The key disabled is harmless JavaScript-wise and has an unmistakable HTML effect.
The returned email field is greyed out and cannot accept input. A string displayed inside value cannot disable its own element, so the key has crossed from request structure into HTML structure.
The response makes that transition explicit. The parameter value becomes value="y", while its key appears separately as disabled="". Escaping the value still works; it cannot protect an attacker-controlled attribute name.
parseEmailShape first looks for the exact scalar name. When that value is absent, it scans every parameter for digest[email][…], captures the text inside the final brackets and returns those key-value pairs as an object email shape.
The validator correctly refuses any email shape that is not scalar. Rejection is only half of this path, though: the page still needs to redisplay the rejected input. The same email object is retained in form state and passed to that renderer.
The object branch preserves its first value, then maps every captured key to an HtmlAttribute name. serializeAttributes escapes each attribute value but interpolates attribute.name directly into the tag.
The implementation’s reasonable assumption is that attribute names are trusted constants and only their values need output encoding. That is true for the normal attributes assembled by the page. It stops being true when restoredAttributes promotes request parameter keys into the same array.
Placing autofocus onfocus=alert(1); in the nested key creates the two attributes needed for execution. The first moves focus to the input in a normal browser tab; the second supplies the event handler.
The serializer appends ="" to every attribute object. With the handler at the end of the key, the browser receives onfocus=alert(1);="". The trailing fragment makes the handler invalid JavaScript, so this form does not execute.
Adding data-x after the handler gives the serializer a separate, inert attribute to finish. The semicolon terminates the JavaScript expression before that final attribute begins.
The returned input now contains a clean onfocus=alert(1); followed by data-x="". The response also contains no Content Security Policy that would block this inline handler.
Focusing the input executes the injected handler and opens the alert. The lab is embedded cross-origin, so Chrome blocks automatic focus inside the frame; opening the crafted URL in a normal tab allows autofocus to trigger without another click.
The route is public. An attacker can present the URL as a CamberStack digest link, and code then runs in CamberStack’s origin in the visitor’s browser. For a signed-in member, that code can read forum pages available to the member and issue same-origin actions with their session.
The manifestation spans two otherwise plausible components. preservedEmailAttributes lets request keys enter an attribute collection, and renderDigestEmailInputHtml spreads that collection into a hand-built tag before passing it to dangerouslySetInnerHTML.
The corrected boundary accepts only a string for the email field. A non-scalar digest[email] must be rejected or coerced before redisplay, and the input should be rendered as a framework element whose attribute names are fixed in source. The submitted email may control the value property; it must never control the set of properties on the element.
Adding more replacements to escapeAttribute would leave the failed boundary intact because the payload travels in attribute.name, not attribute.value. Blocking names that start with on is also brittle; HTML has other structural attributes and parser edge cases.
The transferable review question is whether one logical form field can arrive in more than one type. Bracket-notation parsers in signup, contact, filter and preference forms often produce arrays or objects from names that usually carry strings. Validate that shape before rejected input reaches templates, and keep request-derived keys out of HTML attribute, style and component-prop names.
Every screen and request above came from the live app. In the lab you do the whole thing yourself, hands-on, with guidance along the way.