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

Block (most) canvas and webgl fingerprinting by default #1354

Merged
merged 3 commits into from
Apr 17, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 221 additions & 0 deletions app/extensions/brave/brave-default.js
Original file line number Diff line number Diff line change
Expand Up @@ -763,4 +763,225 @@ if (typeof KeyEvent === 'undefined') {
ipcRenderer.on('post-page-load-run', function () {
ipcRenderer.sendToHost('theme-color-computed', computeThemeColor())
})

/** Begin canvas fingerprinting detection **/
/**
* @return {string}
*/
function getPageScript () {
return '(' + Function.prototype.toString.call(function (ERROR) {
ERROR.stackTraceLimit = Infinity // collect all frames
var event_id = document.currentScript ? document.currentScript.getAttribute('data-event-id') : ''

// from Underscore v1.6.0
function debounce (func, wait, immediate) {
var timeout, args, context, timestamp, result

var later = function () {
var last = Date.now() - timestamp
if (last < wait) {
timeout = setTimeout(later, wait - last)
} else {
timeout = null
if (!immediate) {
result = func.apply(context, args)
context = args = null
}
}
}

return function () {
context = this
args = arguments
timestamp = Date.now()
var callNow = immediate && !timeout
if (!timeout) {
timeout = setTimeout(later, wait)
}
if (callNow) {
result = func.apply(context, args)
context = args = null
}
return result
}
}

// messages the injected script
var send = (function () {
var messages = []
// debounce sending queued messages
var _send = debounce(function () {
document.dispatchEvent(new window.CustomEvent(event_id, {
detail: messages
}))
// clear the queue
messages = []
}, 100)
return function (msg) {
// queue the message
messages.push(msg)
_send()
}
}())

// https://code.google.com/p/v8-wiki/wiki/JavaScriptStackTraceApi
/**
* Customize the stack trace
* @param structured If true, change to customized version
* @returns {*} Returns the stack trace
*/
function getStackTrace (structured) {
var errObj = {}
var origFormatter
var stack

if (structured) {
origFormatter = ERROR.prepareStackTrace
ERROR.prepareStackTrace = function (errObj, structuredStackTrace) {
return structuredStackTrace
}
}

ERROR.captureStackTrace(errObj, getStackTrace)
stack = errObj.stack

if (structured) {
ERROR.prepareStackTrace = origFormatter
}

return stack
}

/**
* Checks the stack trace for the originating URL
* @returns {String} The URL of the originating script (URL:Line number:Column number)
*/
function getOriginatingScriptUrl () {
var trace = getStackTrace(true)

if (trace.length < 2) {
return ''
}

// this script is at 0 and 1
var callSite = trace[2]

if (callSite.isEval()) {
// argh, getEvalOrigin returns a string ...
var eval_origin = callSite.getEvalOrigin()
var script_url_matches = eval_origin.match(/\((http.*:\d+:\d+)/)

return script_url_matches && script_url_matches[1] || eval_origin
} else {
return callSite.getFileName() + ':' + callSite.getLineNumber() + ':' + callSite.getColumnNumber()
}
}

/**
* Strip away the line and column number (from stack trace urls)
* @param script_url The stack trace url to strip
* @returns {String} the pure URL
*/
function stripLineAndColumnNumbers (script_url) {
return script_url.replace(/:\d+:\d+$/, '')
}

/**
* Monitor the reads from a canvas instance
* @param item special item objects
*/
function trapInstanceMethod (item) {
item.obj[item.propName] = (function (orig) {
return function () {
var script_url = getOriginatingScriptUrl()
var msg = {
obj: item.objName,
prop: item.propName,
scriptUrl: stripLineAndColumnNumbers(script_url)
}

// Block the read from occuring; send info to background page instead
console.log('blocking canvas read', msg)
send(msg)
}
}(item.obj[item.propName]))
}

var methods = []
var canvasMethods = ['getImageData', 'getLineDash', 'measureText']
canvasMethods.forEach(function (method) {
var item = {
objName: 'CanvasRenderingContext2D.prototype',
propName: method,
obj: window.CanvasRenderingContext2D.prototype
}

methods.push(item)
})

var canvasElementMethods = ['toDataURL', 'toBlob']
canvasElementMethods.forEach(function (method) {
var item = {
objName: 'HTMLCanvasElement.prototype',
propName: method,
obj: window.HTMLCanvasElement.prototype
}
methods.push(item)
})

var webglMethods = ['getSupportedExtensions', 'getParameter', 'getContextAttributes',
'getShaderPrecisionFormat', 'getExtension']
webglMethods.forEach(function (method) {
var item = {
objName: 'WebGLRenderingContext.prototype',
propName: method,
obj: window.WebGLRenderingContext.prototype
}
methods.push(item)
})

methods.forEach(trapInstanceMethod)

// save locally to keep from getting overwritten by site code
}) + '(Error));'
}

/**
* Executes a script in the page DOM context
*
* @param text The content of the script to insert
* @param data attributes to set in the inserted script tag
*/
function insertScript (text, data) {
var parent = document.documentElement
var script = document.createElement('script')

script.text = text
script.async = false

for (var key in data) {
script.setAttribute('data-' + key.replace('_', '-'), data[key])
}

parent.insertBefore(script, parent.firstChild)
parent.removeChild(script)
}

ipcRenderer.on('block-canvas-fingerprinting', function () {
var event_id = Math.random().toString()

// listen for messages from the script we are about to insert
document.addEventListener(event_id, function (e) {
if (!e.detail) {
return
}
// pass these on to the background page
ipcRenderer.send('got-canvas-fingerprinting', e.detail)
})

insertScript(getPageScript(), {
event_id: event_id
})
})
/* End canvas fingerprinting detection */
}).apply(this)
1 change: 1 addition & 0 deletions app/extensions/brave/locales/en-US/preferences.l20n
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<bookmarks "Bookmarks">
<openedTabs "Open tabs">
<doNotTrack "Send a 'Do Not Track' header with browsing requests (requires browser restart)">
<blockCanvasFingerprinting "Block HTML canvas and WebGL fingerprinting">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be good to add a warning here that it could break some pages.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do

<advancedPrivacySettings "Advanced privacy settings:">

/* Security settings page */
Expand Down
4 changes: 4 additions & 0 deletions app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,10 @@ app.on('ready', () => {
})
})

ipcMain.on(messages.GOT_CANVAS_FINGERPRINTING, (e, details) => {
console.log('got canvas fingerprint block', details)
})

// Setup the crash handling
CrashHerald.init()

Expand Down
1 change: 1 addition & 0 deletions docs/state.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ AppStore
'privacy.history-suggestions': boolean, // Auto suggest for history enabled
'privacy.bookmark-suggestions': boolean, // Auto suggest for bookmarks enabled
'privacy.opened-tab-suggestions': boolean, // Auto suggest for opened tabs enabled
'privacy.block-canvas-fingerprinting': boolean, // Canvas fingerprinting defense
'security.passwords.manager-enabled': boolean // whether to use default password manager
}]
}
Expand Down
1 change: 1 addition & 0 deletions js/about/preferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ class PrivacyTab extends ImmutableComponent {
</SettingsList>
<SettingsList dataL10nId='advancedPrivacySettings'>
<SettingCheckbox dataL10nId='doNotTrack' prefKey={settings.DO_NOT_TRACK} settings={this.props.settings} onChangeSetting={this.props.onChangeSetting}/>
<SettingCheckbox dataL10nId='blockCanvasFingerprinting' prefKey={settings.BLOCK_CANVAS_FINGERPRINTING} settings={this.props.settings} onChangeSetting={this.props.onChangeSetting}/>
</SettingsList>
</div>
}
Expand Down
3 changes: 3 additions & 0 deletions js/components/frame.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,9 @@ class Frame extends ImmutableComponent {
ipc.send(messages.CHECK_CERT_ERROR_ACCEPTED, parsedUrl.host, this.props.frame.get('key'))
}
}
if (getSetting(settings.BLOCK_CANVAS_FINGERPRINTING)) {
this.webview.send(messages.BLOCK_CANVAS_FINGERPRINTING)
}
windowActions.updateBackForwardState(
this.props.frame,
this.webview.canGoBack(),
Expand Down
1 change: 1 addition & 0 deletions js/constants/appConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ module.exports = {
'privacy.bookmark-suggestions': true,
'privacy.opened-tab-suggestions': true,
'privacy.autocomplete.history-size': 500,
'privacy.block-canvas-fingerprinting': false,
'bookmarks.toolbar.show': false,
'privacy.do-not-track': false,
'security.passwords.manager-enabled': true,
Expand Down
2 changes: 2 additions & 0 deletions js/constants/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ const messages = {
LEAVE_FULL_SCREEN: _,
SET_CLIPBOARD: _,
AUTOFILL_PASSWORD: _,
BLOCK_CANVAS_FINGERPRINTING: _,
GOT_CANVAS_FINGERPRINTING: _,
// Password manager
GET_PASSWORDS: _, /** @arg {string} formOrigin, @arg {string} action */
GOT_PASSWORD: _, /** @arg {string} username, @arg {string} password, @arg {string} origin, @arg {string} action, @arg {boolean} isUnique */
Expand Down
1 change: 1 addition & 0 deletions js/constants/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const settings = {
OPENED_TAB_SUGGESTIONS: 'privacy.opened-tab-suggestions',
AUTOCOMPLETE_HISTORY_SIZE: 'privacy.autocomplete.history-size',
DO_NOT_TRACK: 'privacy.do-not-track',
BLOCK_CANVAS_FINGERPRINTING: 'privacy.block-canvas-fingerprinting',
// Security Tab
PASSWORD_MANAGER_ENABLED: 'security.passwords.manager-enabled',
ONE_PASSWORD_ENABLED: 'security.passwords.one-password-enabled',
Expand Down