CSS Injection
Also known as: Style injection, CSS exfiltration, Stylesheet injection
A flaw in which untrusted input is placed into a style context such as an inline style attribute, a style block or a user-controlled theme, letting attacker-supplied styling leak page data or redress the interface; it is prevented by never putting untrusted data in CSS, strict allow-lists and a restrictive Content-Security-Policy.
How it works
CSS injection is a flaw in how a web application builds the styling it sends. When the code places a value that came from outside, such as a theme colour, a custom style field or a profile setting, into an inline style attribute or a style block, the browser cannot tell which rules the developer wrote and which were supplied by a user. Input meant to be a single value can be read as additional styling rules.
The root cause is the familiar injection shape: data and instructions share one channel. The impact is different from script injection because styles cannot run code, yet they are still powerful. Style rules can select elements by their attributes and trigger requests for external resources, which can leak page content such as tokens held in attributes. They can also reposition, hide or overlay interface elements, which enables UI redress and fake prompts. The risk is highest on pages that show sensitive values in the document.
Typical sources are user-controlled themes, custom branding fields, markdown or rich-text features that allow style attributes, and server code that builds a style string from request values. A reviewer looks for any path where untrusted data is concatenated into a style context and asks whether the feature genuinely needs user-supplied styling at all.
Defence is layered. The primary control is to never place untrusted data in CSS. Where users must choose styling, accept only values from a strict allow-list of properties and value formats, and apply them through safe framework styling APIs rather than building CSS strings. A Content-Security-Policy that restricts style-src, img-src and connect-src limits what injected styling can load or leak, and monitoring of CSP reports and outbound requests reveals attempts.
Walk through it
- 1Read the theme code
- 2Identify the sink
- 3Read the signals, pick the fix
- Scope every style sink
- Allow-list, add CSP and verify
A pre-release review flags the board theme feature. Members can pick an accent colour that is applied to the whole board. Read the handler as a reviewer and note how the style text is produced.
1// GET /board?accent=...2router.get("/board", async (req, res) => {3 const accent = req.query.accent;4 const css = "<style>.board { --accent: " + accent + "; }</style>";5 res.send(css + renderBoard(await loadBoard(req)));6});Spot it
- Content-Security-Policy violation reports for img-src, style-src or connect-src from pages that normally produce none.
- Rendered pages issuing bursts of outbound image or resource requests to a host that is not part of the application.
- Style-related parameters or fields containing unusually long or structured values compared with the usual short colour or preset names.
- Stored profile, theme or branding fields that contain stylesheet-like text rather than a simple value.
- Shared links carrying theme or style parameters that appear on unrelated external sites.
CSP report endpoint
ts=2026-10-11T11:20:04Z document-uri=https://boards.contoso.example/board violated-directive=img-src blocked-uri=https://198.51.100.9 disposition=report
ts=2026-10-11T11:20:05Z document-uri=https://boards.contoso.example/board violated-directive=img-src blocked-uri=https://198.51.100.9 disposition=reportProxy egress
ts=2026-10-11T11:20:04Z client=10.8.4.21 method=GET host=198.51.100.9 type=image referrer=https://boards.contoso.example/board
ts=2026-10-11T11:20:05Z client=10.8.4.21 method=GET host=198.51.100.9 type=image referrer=https://boards.contoso.example/boardsplSplunk: pages with a burst of CSP image violations
index=csp sourcetype=csp_report violated_directive="img-src*"
| stats count dc(blocked_uri) as hosts by document_uri
| where count > 50Tune the threshold to the page's baseline. Browser extensions cause some noise, so correlate with egress logs before escalating.
kqlKQL: many image requests to one unfamiliar host from application pages
ProxyLogs
| where Referrer has "/board" and ContentType startswith "image"
| summarize hits=count() by DestinationHost
| where hits > 100Stop it
Never put untrusted data in CSS
Keep request values and stored user text out of style attributes, style blocks and stylesheets. If a feature only needs a choice, store the choice as a preset name and map it to styling on the server.
Use a strict allow-list and safe framework styling
When users must pick styling, accept only specific properties and value formats, such as a hex colour pattern, and apply them through the framework's styling API or a class mapping rather than a string-built stylesheet. Reject anything else.
Add a restrictive Content-Security-Policy and encode output
Set style-src without unsafe inline allowances where practical, and restrict img-src and connect-src to known hosts, so injected styling cannot load or leak to outside destinations. Encode output for its context as a backstop.
Allow-list the value instead of joining it into CSS
Vulnerable
const css = "<style>.board { --accent: " + accent + "; }</style>";
res.send(css + page);Hardened
const HEX = /^#[0-9a-fA-F]{6}$/;
const safe = HEX.test(String(accent)) ? String(accent) : "#2563eb";
res.render("board", { accent: safe }); // template sets the style via a safe APIBetter still, store a preset id such as "blue" and map it to a class, so no free-form value reaches styling at all.
Restrict what styling can load
Hardened
res.setHeader("Content-Security-Policy", "default-src 'self'; style-src 'self'; img-src 'self'; connect-src 'self'; object-src 'none'");Start in report-only mode to learn what the policy would block, then enforce.
- Do not accept free-form CSS from users; offer preset themes or constrained pickers instead.
- Allow-list properties and value formats server-side and reject everything else.
- Apply styling through framework APIs or class mappings, not string-built CSS.
- Deploy a Content-Security-Policy that restricts style-src, img-src and connect-src, with a report endpoint.
- Keep sensitive values out of element attributes on pages that render user-styled content.
- Enforce static-analysis rules for tainted values reaching style sinks in CI, and monitor CSP and egress reports.
If it already happened
Disable the user-controlled theme feature or fall back to preset themes, tighten the Content-Security-Policy for img-src and connect-src, and block the unfamiliar destination at the proxy.
Replace free-form style handling with allow-listed values on every affected sink, clean stored theme or profile fields that contain stylesheet-like text, and rotate any tokens that were present in page attributes.
Run regression tests confirming only allow-listed values are applied, restore altered content, and notify affected users if sensitive values may have been exposed.
Add a static-analysis rule and review checklist item for untrusted data in style contexts, keep the CSP enforced with reporting, and retain the egress detections permanently.
Check yourself
1. What is the root cause of CSS injection?
2. Which approach is the primary fix when a feature lets users choose a theme colour?
3. What does a restrictive Content-Security-Policy with limited img-src and connect-src add?