Skip to contentSellstamp

Security

HTML and CSS yes, JavaScript no — and why that is not a compromise

Merchants asked twice for custom theme code, naming Shopify, Sellix and Tebex. That naming turned out to be the answer rather than an argument against us.

We refused custom theme code once, and the reason held as far as it went: a storefront takes card details, and a <script> pasted from a forum runs on the merchant’s customers rather than on the merchant. Of everything on the original wish list it is the one item that, shipped carelessly, harms somebody other than the person who asked for it.

Then the request came back, twice, naming three platforms that supposedly allow it. We went and looked. None of them lets a theme run arbitrary JavaScript. Shopify themes are Liquid. The others are markup with a template syntax. The feature merchants actually want is markup and styling they control — and that is a feature with a well-understood safe implementation.

The refusal was right about JavaScript and wrong about HTML. Those are two different answers, and we had been giving one.

Where the line sits

HTML and CSS, through an allow-list: structure, text, links, images, tables, classes and inline styles. JavaScript, in no form at all — script tags, every event handler attribute, javascript: and data: hrefs, iframes including inline documents, objects, embeds, form elements, meta refresh and base tags.

The form elements are excluded for their own reason, separate from the scripting one. This platform’s forms take money. An unrelated input beside a real one is a phishing surface on the merchant’s own domain, and a merchant who pasted the markup in good faith would be the one hosting it.

An allow-list, never a block-list. A block-list is a list of the attacks somebody thought of. This passes through exactly what it names and drops everything else, including whatever gets invented next month.

CSS is scoped, not sanitised

A merchant’s stylesheet is wrapped so it applies to their section and nothing else. Without that, “custom CSS” is a tool for hiding the price, restyling the checkout button, or overlaying the page — on a page that takes card details. Braces are counted rather than parsed: a block that does not close cannot escape its wrapper, and one that closes too many times is rejected whole rather than allowed to leak.

One boundary, on the only thing that writes

Sanitisation happens on the way in, in the API, and nowhere else. The stored value is already safe. That is a decision about where, not a shortcut: the storefront renderer is imported by the editor, which is a browser component, so a sanitiser on the render path would ship a parser into every merchant’s browser and still not be the authority. Both write paths are covered — the ordinary save and the restore-a-previous-version path — and both are tested.

Those tests are an attack list of fifteen techniques, asserted against what was stored rather than against what the endpoint answered. Plus one that is easy to forget: removing a tag must not eat the merchant’s words.

Drawn from ADR-0141, the decision record written before this was built. Every non-obvious choice in this product has one.

Open a store and try it.

Everything argued here is in the product today. Connect a provider in test mode and put a real order through it before you decide.