You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: src/pentesting-web/xs-search/event-loop-blocking-+-lazy-images.md
+45-1Lines changed: 45 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -11,6 +11,8 @@ This is a **different exploit for the CTF chall** that was already commented in
11
11
connection-pool-example.md
12
12
{{#endref}}
13
13
14
+
This technique is useful when the attacker can create a **Boolean oracle** based on whether a **lazy-loaded image** is fetched or not, but **cannot** directly observe that request because of CSP, `img-src` restrictions, or `Cache-Control: no-store`. Instead of waiting for an external callback, the exploit converts image loading into a **timing side channel** by making those image requests compete with other requests.
15
+
14
16
The idea behind this exploit is:
15
17
16
18
- The posts are loaded alphabetically
@@ -20,6 +22,12 @@ The idea behind this exploit is:
20
22
- If the **images** injected in the post are being **loaded**, these **fetch** requests will take **longer**, so the attacker knows that the **post is before the flag** (alphabetically).
21
23
- If the the **fetch** requests are **fast**, it means that the **post** is **alphabetically****after** the flag.
22
24
25
+
In other words, the oracle is:
26
+
27
+
-**State 1**: the attacker-controlled post is within the browser lazy-loading threshold, so `img loading=lazy` requests are issued.
28
+
-**State 2**: the attacker-controlled post remains outside that threshold, so those requests are not issued.
29
+
-**Leak**: the attacker measures whether those extra requests create enough contention to delay another measurable operation.
30
+
23
31
Let's check the code:
24
32
25
33
```html
@@ -154,6 +162,42 @@ Let's check the code:
154
162
</html>
155
163
```
156
164
157
-
{{#include ../../banners/hacktricks-training.md}}
165
+
## Practical caveats
166
+
167
+
This trick is **fragile** and needs to be **calibrated per environment**:
168
+
169
+
- The **lazy-loading distance threshold** is browser-dependent and can change with browser version, connection type, and headless/headful mode. Chromium loads off-screen images **before** they are visible, so the right `<canvas height>` is usually found empirically.
170
+
- In practice, **headless Chromium** can require a **different threshold** than a normal browser. In the original writeup, a value that worked locally (`1850px`) had to be increased for the remote headless bot (`3350px`).
171
+
- Native `loading="lazy"` is only **deferred when JavaScript is enabled**, so this specific oracle can disappear if the browser disables JS or changes lazy-loading behavior for privacy reasons.
172
+
- If the image response is **cacheable**, later probes become noisy or useless because the browser may satisfy the request from cache. This is why cache-busting parameters or `Cache-Control: no-store` matter a lot when testing this technique.
173
+
174
+
## Reliability notes
175
+
176
+
Compared to the related [connection pool example](connection-pool-example.md), this variant does **not** need an external image callback. It only needs a measurable slowdown. That slowdown can come from:
177
+
178
+
-**Server-side event-loop blocking**, such as many image requests hitting a Node.js endpoint that performs synchronous work.
179
+
-**Socket / connection contention**, where the attacker saturates available connections and times how long an additional request takes.
180
+
181
+
To make the oracle more stable:
158
182
183
+
- Use **multiple lazy images** instead of one.
184
+
- Add a **cache-buster** to every image URL.
185
+
- Measure **several requests** and compare an **average/median** instead of trusting a single sample.
186
+
- Recalculate the **canvas height threshold** against the same browser family and execution mode used by the victim bot.
187
+
188
+
For more timing-based leak primitives, also check:
189
+
190
+
{{#ref}}
191
+
performance.now-+-force-heavy-task.md
192
+
{{#endref}}
193
+
194
+
> [!WARNING]
195
+
> Some privacy-focused defenses can break the "load only after a browser-driven scroll/viewport change" assumption. For example, XS-Leaks wiki documents `Document-Policy: force-load-at-top` as a way to disable load-on-scroll behaviors such as Scroll-to-Text navigation, which can also reduce similar viewport-based oracles.
0 commit comments