content-visibility: hidden does not skip background-image loading (unlike display:none)
Categories
(Core :: CSS Parsing and Computation, defect, P3)
Tracking
()
People
(Reporter: ialleejy, Unassigned)
References
(Blocks 1 open bug)
Details
Attachments
(2 files)
User Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36
Steps to reproduce:
- Create a minimal HTML page with three elements:
<div style="width:1px;height:1px;background-image:url('/probe?c=visible')"></div>
<div style="display:none">
<div style="width:1px;height:1px;background-image:url('/probe?c=display-none')"></div>
</div>
<div style="content-visibility:hidden;contain-intrinsic-size:0 0">
<div style="width:1px;height:1px;background-image:url('/probe?c=cv-hidden')"></div>
</div>
- Serve it with any HTTP server and open in Firefox 148.0.2.
- Check server logs or DevTools Network tab for which background-image URLs were requested.
A Dockerized PoC is attached (poc-docker.zip). Run:
docker compose up --build
Actual results:
Firefox requested background-image for both the visible element and the cv:hidden element:
/probe?c=visible ← expected
/probe?c=cv-hidden ← unexpected
/probe?c=display-none was correctly NOT requested.
Firefox skips background-image loading for display:none but loads it for content-visibility:hidden.
Expected results:
background-image inside content-visibility:hidden should NOT be loaded, consistent with display:none behavior.
Only /probe?c=visible should be requested.
MDN describes content-visibility:hidden as "similar to giving the contents display:none". Firefox already optimizes display:none to skip background-image loading. The same optimization should apply to content-visibility:hidden.
Chromium 145 correctly skips background-image loading for both display:none and content-visibility:hidden.
| Reporter | ||
Comment 1•4 months ago
|
||
Comment on attachment 9555891 [details]
poc-docker.zip
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0
Also tested on: Windows 10, Firefox 148.0.2 (normal mode) — same behavior.
Root cause analysis:
Firefox's image visibility system decides whether to load background-image based on frame existence. display:none elements have no frames, so images are skipped. content-visibility:hidden elements DO create frames (only rendering is skipped), so the image visibility system loads images for them. This gap has existed since content-visibility shipped in Firefox 125 (April 2024).
Security note:
This inconsistency enables CSS-only data exfiltration from cv:hidden content. An attacker who can inject CSS can use attribute selectors + background-image to extract hidden data character by character, without JavaScript, bypassing CSP script-src restrictions. The attached exfil-demo.js demonstrates this.
Cross-browser comparison:
display:none bg → Firefox: SKIP, Chromium: SKIP
cv:hidden bg → Firefox: LOAD, Chromium: SKIP
Attached PoC (poc-docker.zip):
- test.js: Compares display:none vs cv:hidden resource loading
- exfil-demo.js: Extracts a secret from a cv:hidden element via CSS
- Run: docker compose up --build
| Reporter | ||
Comment 2•4 months ago
|
||
Additional findings: The resource loading gap extends far beyond background-image.
After further investigation, the content-visibility:hidden resource loading issue affects ALL image-type CSS resource properties, not just background-image.
Affected resource types (cv:hidden LOADS, display:none correctly SKIPS):
- background-image
- list-style-image
- border-image-source
- mask-image
- cursor: url()
- shape-outside: url()
- content: url()
7 out of 7 resource types show mismatch. Only @font-face is correctly skipped by both.
Additionally, content-visibility:auto also loads resources for elements far off-screen (99999px below viewport) without any scrolling, defeating its primary purpose.
Updated PoC attached (comprehensive-poc.zip). Run: docker compose build && docker run --rm poc-docker-poc node comprehensive-poc.js
Updated•3 months ago
|
Comment 3•3 months ago
|
||
I don't think this is specified, but I agree it'd be nice to avoid these. We don't avoid triggering these on visibility: hidden stuff FWIW, so would be good to fix that as well while at it.
We trigger image loads off DidSetComputedStyle here.
We could do that conditionally on us not being hidden. For content-visibility: auto it's a bit trickier. Basically we'd need to start image loads in the subtree when the node becomes relevant.
Another potential approach is to trigger the load more lazily altogether (from painting or so?), just like we trigger decode...
| Reporter | ||
Comment 4•3 months ago
|
||
I built a proof of concept showing that this behavior has security implications beyond a resource-loading inconsistency.
When resource-bearing CSS properties still produce network-observable side effects inside a content-visibility:hidden subtree, the hidden subtree can participate in CSS-only exfiltration workflows — including FontLeak-style text extraction using crafted ligature fonts and container queries, without JavaScript execution.
Under a CSS-injection threat model, if sensitive text is present in hidden DOM (for example, tokens or other high-value data), this behavior can turn otherwise non-rendered content into a practical exfiltration target.
Because this bug is public, I am not posting the full PoC here. I can provide a reduced, self-contained Docker-based demonstration in a restricted follow-up bug if that would be useful for security evaluation.
Would it make sense to request security triage or open a restricted follow-up bug focused on the exfiltration aspect?
Comment 5•3 months ago
|
||
Please file a separate security bug, we can link it to this one. But I'm a bit skeptic about the validity of such test-case. Things with opacity: 0; pointer-events: none;, or visibility: hidden, or so, have similar behavior. Some of those image requests you can't even avoid because of shape-outside for example, which affects layout, not just rendering... But anyways let's continue this discussion in the other bug when you file it. Thanks!
| Reporter | ||
Comment 6•3 months ago
|
||
Thanks. I filed a separate restricted security bug for the security angle: bug 2030911.
I’ll keep the security-specific details there and leave this bug focused on the functional behavior.
Comment 7•3 months ago
|
||
Thanks! Can you cc me in that bug? Otherwise I need to wait for the security team to triage it to be able to see it and link it here :)
| Reporter | ||
Comment 8•3 months ago
|
||
Sure — I’ve added you to the security bug so you should be able to see it now.
Description
•