Skip to content
This repository has been archived by the owner on Dec 11, 2019. It is now read-only.

Commit

Permalink
block access to child frames' contentWindow, contentDocument, place g…
Browse files Browse the repository at this point in the history
…uard in blocking proxy to prevent an infinite loop, issue #11683
  • Loading branch information
pes10k committed Nov 17, 2017
1 parent 39d7cb8 commit 5bec718
Show file tree
Hide file tree
Showing 3 changed files with 268 additions and 46 deletions.
266 changes: 220 additions & 46 deletions app/extensions/brave/content/scripts/blockCanvasFingerprinting.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

if (chrome.contentSettings.canvasFingerprinting == 'block') {

Error.stackTraceLimit = Infinity // collect all frames

// https://code.google.com/p/v8-wiki/wiki/JavaScriptStackTraceApi
Expand Down Expand Up @@ -46,12 +47,14 @@ if (chrome.contentSettings.canvasFingerprinting == 'block') {
function getOriginatingScriptUrl () {
var trace = getStackTrace(true)

if (trace.length < 3) {
if (trace.length < 4) {
return ''
}

// this script is at 0 and 1
var callSite = trace[2]
// This script is at positions 0 (this function), 1 (the reportBlock
// function) and 2 (the newGlobalFunction function). Index 3 and beyond
// are on the client page.
var callSite = trace[3]

if (callSite.isEval()) {
// argh, getEvalOrigin returns a string ...
Expand Down Expand Up @@ -96,6 +99,18 @@ if (chrome.contentSettings.canvasFingerprinting == 'block') {
return (possiblePropDesc && !possiblePropDesc.configurable)
})

/**
* Returns a false-y default value, depending on how a proxy is being coerced.
*
* @param {String} hint
* A string, describing the type of coercion being applied to the proxy.
* Expected values are "string", "number" or "default".
*
* @return {String|Number|undefined}
* A falsey value, determined by the type of coercion being applied.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive
*/
var valueOfCoercionFunc = function (hint) {
if (hint === 'string') {
return ''
Expand All @@ -106,70 +121,158 @@ if (chrome.contentSettings.canvasFingerprinting == 'block') {
return undefined
}

var allPurposeProxy = new Proxy(defaultFunc, {
get: function (target, property) {
// This object is used to map from names of blocking proxy objects,
// to the proxy object itself. Its used to allow the handler of each
// proxy to refer to the proxy before the proxy is created (since the
// handler is needed to create the proxy, there would otherwise be a
// chicken and egg situation).
var proxyRegistery = {}

if (property === Symbol.toPrimitive) {
return valueOfCoercionFunc
/**
* Returns a handler object, for use in configuring a Proxy object.
*
* The returned handler will return a named proxy on get, set and apply
* operations up to `loopGuardMax` times (1000), and then returns undefined
* to avoid infinite loops.
*
* @param {String} proxyName
* The name of the proxy ("registered" in the above "proxyRegistery"
* object) to return when the proxy object is {get,set,apply}'ed.
* @param {?Function(String)} onTriggerCallback
* An optional callback funciton that will be called whenever the
* proxy object is get,'ed. This callback is called with
* a single argument, the name of the property being looked up.
*
* @return {Object}
* A valid Proxy handler definition.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler
*/
var createProxyHandler = function (proxyName, onTriggerCallback) {

var loopGuardMax = 1000
var callCounter = 0

return {
get: function (target, property) {

if (onTriggerCallback !== undefined) {
onTriggerCallback(property)
}

// If the proxy has been called a large number of times on this page,
// it might be stuck in an loop. To prevent locking up the page,
// return undefined to break the loop, and then resume the normal
// behavior on subsequent calls.
if (callCounter > loopGuardMax) {
callCounter = 0
return undefined
}

callCounter += 1

if (property === Symbol.toPrimitive) {
return valueOfCoercionFunc
}

if (property === 'toString') {
return ''
}

if (property === 'valueOf') {
return 0
}

return proxyRegistery[proxyName]
},
set: function () {
return proxyRegistery[proxyName]
},
apply: function () {
return proxyRegistery[proxyName]
},
ownKeys: function () {
return unconfigurablePropNames
},
has: function (target, property) {
return (unconfigurablePropNames.indexOf(property) > -1)
},
getOwnPropertyDescriptor: function (target, property) {
if (unconfigurablePropNames.indexOf(property) === -1) {
return undefined
}
return Object.getOwnPropertyDescriptor(defaultFunc, property)
}
}
}

if (property === 'toString') {
return ''
}
var mainFrameBlockingProxy = new Proxy(defaultFunc, createProxyHandler("mainFrameBlockingProxy"))
proxyRegistery["mainFrameBlockingProxy"] = mainFrameBlockingProxy

if (property === 'valueOf') {
return 0
}
/**
* Reports that a method related to fingerprinting was called by the page.
*
* @param {String} type
* A category of the fingerprinting method, such as "SVG", "iFrame" or
* "Canvas".
* @param {?String} scriptUrlToReport
* Optional URL to report where the fingerprinting effort happened.
* If this is undefined, then the script URL is determined from
* a stack trace.
*/
function reportBlock (type, scriptUrlToReport) {

return allPurposeProxy
},
set: function () {
return allPurposeProxy
},
apply: function () {
return allPurposeProxy
},
ownKeys: function () {
return unconfigurablePropNames
},
has: function (target, property) {
return (unconfigurablePropNames.indexOf(property) > -1)
},
getOwnPropertyDescriptor: function (target, property) {
if (unconfigurablePropNames.indexOf(property) === -1) {
return undefined
}
return Object.getOwnPropertyDescriptor(defaultFunc, property)
}
})
var scriptUrl

function reportBlock (type) {
var script_url = getOriginatingScriptUrl()
if (script_url) {
script_url = stripLineAndColumnNumbers(script_url)
if (scriptUrlToReport !== undefined) {
scriptUrl = scriptUrlToReport
} else {
script_url = window.location.href
scriptUrl = getOriginatingScriptUrl()
if (scriptUrl) {
scriptUrl = stripLineAndColumnNumbers(scriptUrl)
} else {
scriptUrl = window.location.href
}
}

var msg = {
type,
scriptUrl: stripLineAndColumnNumbers(script_url)
scriptUrl: stripLineAndColumnNumbers(scriptUrl)
}

// Block the read from occuring; send info to background page instead
chrome.ipcRenderer.sendToHost('got-canvas-fingerprinting', msg)

return allPurposeProxy
}

/**
* Monitor the reads from a canvas instance
* @param item special item objects
* Replaces global method with one that reports the fingerprinting attempt.
*
* @param {Object} item
* A definition for the method that should be replaced.
* @param {String} item.type
* A category for the type of fingerprinting method being blocked, such
* as "SVG" or "Canvas"
* @param {?String} item.propName
* The name of the property / method to be modified. If provided,
* item.objName must also be provided.
* @param {?String} item.objName
* The name of the global structure to modfiy. If this is provided,
* then the item.propName on the prototype of this object will be modified.
* @param {?String} item.methodName
* A global, singleton method to overwrite. If this is provided,
* then item.propName and item.objName are ignored.
*/
function trapInstanceMethod (item) {

var newGlobalFunction = function () {
reportBlock(item.type)
return mainFrameBlockingProxy
}

if (!item.methodName) {
chrome.webFrame.setGlobal(item.objName + ".prototype." + item.propName, reportBlock.bind(null, item.type))
chrome.webFrame.setGlobal(item.objName + ".prototype." + item.propName, newGlobalFunction)
} else {
chrome.webFrame.setGlobal(item.methodName, reportBlock.bind(null, item.type))
chrome.webFrame.setGlobal(item.methodName, newGlobalFunction)
}
}

Expand Down Expand Up @@ -267,4 +370,75 @@ if (chrome.contentSettings.canvasFingerprinting == 'block') {
type: 'WebRTC',
methodName: 'navigator.mediaDevices.enumerateDevices'
})

var propertiesToReportInIframe = new Set(canvasMethods
.concat(canvasElementMethods)
.concat(webglMethods)
.concat(audioBufferMethods)
.concat(analyserMethods)
.concat(svgPathMethods)
.concat(svgTextContentMethods)
.concat(webrtcMethods)
.concat(['enumerateDevices']))

// Boolean guard used to make sure we don't report more than one iframe
// fingerprinting attempt per page.
var hasiFrameReported = false

/**
* Callback function called when the iframe proxy receives a "get".
*
* This callback is used to report fingerprinting attempts using methods
* extracted from an iframe.
*
* @param {String} property
* The name of the propery being "get" in the iframe proxy.
*
* @return {Boolean}
* true if a fingerprinting attempt was reported, otherwise false
*/
var onIframeProxyCalled = function (property) {

if (hasiFrameReported === true) {
return false
}

if (propertiesToReportInIframe.has(property) === false) {
return false
}

hasiFrameReported = true
reportBlock("Iframe", window.location.href)
return true
}

var iframeBlockingProxy = new Proxy(defaultFunc, createProxyHandler("iframeBlockingProxy", onIframeProxyCalled))
proxyRegistery["iframeBlockingProxy"] = iframeBlockingProxy

chrome.webFrame.setGlobal("window.__braveIframeProxy", iframeBlockingProxy)

// Prevent access to frames' contentDocument / contentWindow
// properties, to prevent the parent frame from pulling unblocked
// references to blocked standards from injected frames.
// This may break some sites, but, fingers crossed, its not too much.
var pageScriptToInject = function () {
var frameTypesToModify = [window.HTMLIFrameElement, window.HTMLFrameElement]
var propertiesToModify = ['contentWindow', 'contentDocument']
var braveIframeProxy = window.__braveIframeProxy
delete window.__braveIframeProxy

frameTypesToModify.forEach(function (frameType) {
propertiesToModify.forEach(function (propertyToModify) {
Object.defineProperty(frameType.prototype, propertyToModify, {
get: () => {
// XXX: this breaks contentWindow.postMessage since the target window
// is now the parent window
return braveIframeProxy
}
})
})
})
}

chrome.webFrame.executeJavaScript(`(${pageScriptToInject.toString()})()`)
}
14 changes: 14 additions & 0 deletions test/bravery-components/braveryPanelTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,20 @@ describe('Bravery Panel', function () {
.tabByIndex(0)
.waitUntil(verifyProxyBlocking)
})
it('blocking access to fingerprinting methods on iframe.contentWindow', function * () {
const url = Brave.server.url('fingerprinting-blocking-from-child-frames.html')
yield this.app.client
.tabByIndex(0)
.loadUrl(url)
.waitForUrl(url)
.openBraveMenu(braveMenu, braveryPanel)
yield changeFpSetting(this.app.client, blockFpOption)
yield this.app.client
.waitForTextValue(fpStat, '1')
.keys(Brave.keys.ESCAPE)
.tabByIndex(0)
.waitUntil(verifyProxyBlocking)
})
it('block device enumeration', function * () {
const url = Brave.server.url('enumerate_devices.html')
yield this.app.client
Expand Down
34 changes: 34 additions & 0 deletions test/fixtures/fingerprinting-blocking-from-child-frames.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<html>
<head>
</head>
<body>
<div id="target">
proxy blocking being tested
</div>
<script>
(function () {

const targetElm = document.getElementById("target")
const iframe = document.createElement("iframe")
iframe.src = "https://example.com/"
document.body.appendChild(iframe)

// Accessing the `toDataUrl` property on the iframe-content-wrapping
// proxy should trigger a finger printing notification.
let iframeToDataUrl = iframe.contentWindow.HTMLCanvasElement.prototype.toDataURL

// If this is the proxied version of the toDataURL method, then
// we can look up any properties on it w/o throwing an exception.
// The below throwing means the proxy isn't injected correctly,
// and so the test should fail.

try {
iframeToDataUrl.nonexistant().properties['and'].methods()
targetElm.innerText = "proxy blocking works"
} catch (e) {
targetElm.innerText = "proxy blocking fail"
}
}())
</script>
</body>
</html>

0 comments on commit 5bec718

Please sign in to comment.