CSS Injection Reference
When you have only CSS injection, here are some techniques to leak info. Click on a link to see an example. None of these techniques are due to me.
Matching
Attribute Selectors: You can use CSS attribute selectors to match attributes of an HTML element, including CSRF tokens, etc. Hidden types cannot be directly styled, but you can use pseudo-selectors like :has()
or combinators like ~
.
Errors: You can style a node that only displays if something fails to load (i.e. image text node).
Lazy Loading: You can style elements that only load when they are on the screen like images.
Scrolling: You can style the target of a scroll using Scroll-To-Id or even Scroll-To-Text-Fragment given interaction, using :target
. If the scroll does not happen, the selector will not load.
Text Nodes
Ligatures
Text node contents can be leaked using font ligatures. If the element is not normally visible, style the element to display: block
. The font must cause the size of the element to vary if the text matches, i.e. 0 width on all glyphs except the ligature. There appears to be a maximum glyph width. I reccomended including the font inline if possible. The CSP must not restrict font-src. Inline fonts are governed by applicable URL scheme, i.e. data:
or blob:
.
A side channel is needed to determine the size of the div. These are the techniques I'm aware of:
- Lazy load elements, as in the first category, that aren't pushed out by the font when the ligature matches (or vice versa if you're careful with setting up the font). This may require some markup injection and may be slow since you will only get reports if the ligature doesn't match. You may also need to trigger a cache of the stylesheet before the page is loaded to prevent the element from being "flashed" and loaded while the stylesheet is parsing.
- Style --webkit-scrollbar
and have overflow scroll so that the scrollbar style is only loaded if the div is large. Not supported on Firefox.
- Use CSS container queries to check the size of an element that's affected by the size of the text node. Firefox 110+.
You can use the unicode-range
property of a @font-face
to make a font load only trigger when characters match. Note there does not appear to be a way to determine the count of each character, so you can only iteratively determine new characters.
Connections from CSS
URLs that you can load directly in CSS are:
- Background images using background-image
, content
, or many more. The img-src must allow the remote server's URL. In the past, Firefox loaded SVG filters under the default-src directive, but that no longer seems to be the case.
- Fonts are lazy loaded, so you can define a font and only use that font-family in the target. The font-src must allow the remote server's URL.
Other
You can (ab)use @import
with a server than only resolves the import after some time to leak repeatedly on the same page.
An @import
can leak the URL with the Referer header. The default Referrer-Policy for stylesheets only sends the origin when making cross-origin requests, which heavily mitigates this.
It used to be reasonable to leak history with the :visited
selector, but now browsers have strict defenses against this. XS-Leaks Wiki has more discussion.