const addTest = (function(){ "use strict"; const statusDefinitions = [ {className: "notRun", text: "not run"}, {className: "loud", text: "API tampering detected"}, {className: "stealthy", text: "no API tampering detected"}, {className: "failed", text: "test failed"} ]; const ul = document.getElementById("tests"); return async function addTest(name, func){ const li = document.createElement("li"); const nameNode = document.createElement("span"); nameNode.className = "name"; nameNode.textContent = name; nameNode.title = func.toString(); li.appendChild(nameNode); li.appendChild(document.createTextNode(": ")); const statusNode = document.createElement("span"); statusNode.className = "status"; li.appendChild(statusNode); ul.appendChild(li); const logs = []; function log(){ logs.push(Array.prototype.slice.call(arguments).join(" ")); } let status = 0; try { status = await func(log)? 1: 2; } catch (error){ console.log(error); status = 3; } li.className = statusDefinitions[status].className; statusNode.textContent = statusDefinitions[status].text; statusNode.title = logs.join("\n"); return li; }; }()); function checkPropertyDescriptor(object, name, expectedDescriptor, log){ "use strict"; const descriptor = Object.getOwnPropertyDescriptor(object, name); let detected = false; function logProperty(desc, got, expected){ log("Wrong", desc, "for", name, "- got:", got, "- expected: ", expected); } function compare(desc, getter){ const got = getter(descriptor); const expected = getter(expectedDescriptor); if ((typeof expected) === "function"){ if (got.name !== expected.name){ detected = true; logProperty(desc + " (function name)", got.name, expected.name); } if (got.length !== expected.length){ detected = true; logProperty(desc + " (function length)", got.length, expected.length); } const re = "^\\s*function " + expected.name + "\\s*\\(\\)\\s*\\{\\s*\\[native code\\]\\s*\\}\\s*$"; if (!got.toString().match(new RegExp(re))){ detected = true; logProperty(desc + " (function string)", got.toString(), re); } } else if (got !== expected){ logProperty(desc, got, expected); detected = true; } } compare("descriptor type", function(v){return typeof v;}); if (descriptor){ Object.keys(descriptor).forEach(function(key){ compare(key, function(v){return v[key];}); }); } return detected; } addTest("function length", function(log){ "use strict"; if (CanvasRenderingContext2D.prototype.getImageData.length !== 4){ log("expected 4 parameters for getImageData - got", CanvasRenderingContext2D.prototype.getImageData.length); return true; } else { return false; } }); async function getIframe(){ "use strict"; return new Promise(function(resolve){ const length = window.length; const iframe = document.createElement("iframe"); iframe.style.display = "none"; iframe.src = "?"; document.body.appendChild(iframe); iframe.addEventListener("load", function(){ resolve(window[length]); }); }); } addTest("function code", async function(log){ "use strict"; let codeDetected = false; const iframe = await getIframe(); function checkFunctionCode(func, expectedName){ log("checking", expectedName); const reg = new RegExp("^\\s*function " + expectedName + "\\s*\\(\\)\\s*\\{\\s*\\[native code\\]\\s*\\}\\s*$"); if (!func.toString().match(reg)){ log("unexpected function code:", func.toString()); return true; } const iframeString = iframe.Function.prototype.toString.call(func); if (!iframeString.match(reg)){ log("unexpected function code (iframe):", iframeString); return true; } return false; } log("canvas functions"); codeDetected = checkFunctionCode( CanvasRenderingContext2D.prototype.getImageData, "getImageData" ) || codeDetected; codeDetected = checkFunctionCode( HTMLCanvasElement.prototype.toDataURL, "toDataURL" ) || codeDetected; log("history getter"); codeDetected = checkFunctionCode( history.__lookupGetter__("length"), "(get )?length" ) || codeDetected; log("window getters"); codeDetected = checkFunctionCode( window.__lookupGetter__("name"), "(get )?name" ) || codeDetected; codeDetected = checkFunctionCode( window.__lookupSetter__("name"), "(set )?name" ) || codeDetected; log("navigator getters"); Object.keys(navigator.__proto__).forEach(function(property){ if (typeof navigator[property] === "string"){ codeDetected = checkFunctionCode( navigator.__proto__.__lookupGetter__(property), "(get )?" + property ) || codeDetected; } }); log("DOMRect getters"); ["x", "y", "height", "width"].forEach(function(property){ codeDetected = checkFunctionCode( DOMRect.prototype.__lookupGetter__(property), "(get )?" + property ) || codeDetected; }); return codeDetected; }); addTest("toString modified", function(log){ "use strict"; return checkPropertyDescriptor( HTMLCanvasElement.prototype.toDataURL, "toString", undefined, log ) | checkPropertyDescriptor( Object.prototype, "toString", { value: function toString(){}, writable: true, enumerable: false, configurable: true }, log ) | checkPropertyDescriptor( Function.prototype, "toString", { value: function toString(){}, writable: true, enumerable: false, configurable: true }, log ); }); addTest("function name", function(log){ "use strict"; function checkName({func, expectedName}){ if (func.name !== expectedName){ log("unexpected function name: " + func.name + " !== " + expectedName); return true; } else { return false; } } return [ { func: HTMLCanvasElement.prototype.toDataURL, expectedName: "toDataURL" }, { func: CanvasRenderingContext2D.prototype.getImageData, expectedName: "getImageData" }, { func: history.__lookupGetter__("length"), expectedName: "get length" }, { func: window.__lookupGetter__("name"), expectedName: "get name" }, { func: window.__lookupSetter__("name"), expectedName: "set name" }, { func: HTMLIFrameElement.prototype.__lookupGetter__("contentDocument"), expectedName: "get contentDocument" }, { func: HTMLIFrameElement.prototype.__lookupGetter__("contentWindow"), expectedName: "get contentWindow" }, ].map(checkName).some(function(b){return b;}); }); addTest("exposed getters or setters", function(log){ "use strict"; return Object.keys(window).filter(function(key){ if (/^(get|set) /.test(key)){ log("found exposed function", JSON.stringify(key)); return true; } return false; }).length !== 0; }); addTest("property descriptor", function(log){ "use strict"; const properties = [ { object: CanvasRenderingContext2D.prototype, name: "getImageData", descriptor: { value: function getImageData(x, y, w, h){}, writable: true, enumerable: true, configurable: true } }, { object: HTMLCanvasElement.prototype, name: "toDataURL", descriptor: { value: function toDataURL(){}, writable: true, enumerable: true, configurable: true } }, { object: Element.prototype, name: "getClientRects", descriptor: { value: function getClientRects(){}, writable: true, enumerable: true, configurable: true } }, ]; return properties.reduce(function(pass, property){ return checkPropertyDescriptor(property.object, property.name, property.descriptor, log) || pass; }, false); }); addTest("error provocation 1", function(log){ "use strict"; const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); let canvasBlocker = false; try{ ctx.getImageData(0, 0, 0, 0); } catch (error){ try { log(error.name); log(error.toString()); } catch (error){ canvasBlocker = true; } } return canvasBlocker; }); addTest("error provocation 2", function(log){ "use strict"; const canvas = document.createElement("canvas"); canvas.width = 0; const ctx = canvas.getContext("2d"); let canvasBlocker = false; try{ ctx.getImageData(0, 0, 1, 1); log("no error provoked"); } catch (error){ try { log(error.name); log(error.toString()); } catch (error){ canvasBlocker = true; } } return canvasBlocker; }); addTest("error provocation 3", function(log){ "use strict"; let canvasBlocker = false; try{ CanvasRenderingContext2D.prototype.getImageData.apply(undefined, [0, 0, 1, 1]); } catch (error){ try { log(error.name); log(error.toString()); } catch (error){ canvasBlocker = true; } } return canvasBlocker; }); addTest("error properties", function(log){ "use strict"; let canvasBlocker = false; try{ CanvasRenderingContext2D.prototype.getImageData.apply(undefined, [0, 0, 1, 1]); } catch (error){ try { const name = "TypeError"; if (error.name !== name && error instanceof TypeError){ log("Error name wrong. Expected: ", name, "- got:", error.name); canvasBlocker = true; } const start = "@" + location.href.replace(/\.html$/, ".js"); if (!error.stack.startsWith(start)){ log("Error stack starts wrong. Expected:", start, "- got :", error.stack.split(/\n/g, 2)[0]); canvasBlocker = true; } const message = "'getImageData' called on an object that " + "does not implement interface CanvasRenderingContext2D."; if (error.message !== message){ log("Error message wrong. Expected: ", message, "- got:", error.message); canvasBlocker = true; } } catch (error){ canvasBlocker = true; } } return canvasBlocker; }); function testKnownPixelValue(size, log){ "use strict"; const canvas = document.createElement("canvas"); canvas.height = size; canvas.width = size; const context = canvas.getContext("2d"); const imageData = new ImageData(canvas.width, canvas.height); const pixelValues = imageData.data; for (let i = 0; i < imageData.data.length; i += 1){ if (i % 4 !== 3){ pixelValues[i] = Math.floor(256 * Math.random()); } else { pixelValues[i] = 255; } } context.putImageData(imageData, 0, 0); const p = context.getImageData(0, 0, canvas.width, canvas.height).data; for (let i = 0; i < p.length; i += 1){ if (p[i] !== pixelValues[i]){ log("wrong value", p[i], "at", i, "expected", pixelValues[i]); return true; } } return false; } addTest("known pixel value test 1", function(log){ "use strict"; return testKnownPixelValue(1, log); }); addTest("known pixel value test 10", function(log){ "use strict"; return testKnownPixelValue(10, log); }); addTest("double readout test", function(log){ "use strict"; const canvas = document.createElement("canvas"); const context = canvas.getContext("2d"); const imageData = context.getImageData(0, 0, canvas.width, canvas.height); for (let i = 0; i < imageData.data.length; i += 1){ if (i % 4 !== 3){ imageData.data[i] = Math.floor(256 * Math.random()); } else { imageData.data[i] = 255; } } context.putImageData(imageData, 0, 0); const imageData1 = context.getImageData(0, 0, canvas.width, canvas.height); const imageData2 = context.getImageData(0, 0, canvas.width, canvas.height); for (let i = 0; i < imageData2.data.length; i += 1){ if (imageData1.data[i] !== imageData2.data[i]){ log("mismatch at", i, ":", imageData1.data[i], "(", imageData1.data[i].toString(2), ")", "!=", imageData2.data[i], "(", imageData2.data[i].toString(2), ")", "| original:", imageData.data[i], "(", imageData.data[i].toString(2), ")" ); return true; } } return false; }); addTest("double readout test (toDataURL)", function(log){ "use strict"; const canvas = document.createElement("canvas"); const context = canvas.getContext("2d"); const imageData = context.getImageData(0, 0, canvas.width, canvas.height); for (let i = 0; i < imageData.data.length; i += 1){ if (i % 4 !== 3){ imageData.data[i] = Math.floor(256 * Math.random()); } else { imageData.data[i] = 255; } } context.putImageData(imageData, 0, 0); const dataURL1 = canvas.toDataURL(); const dataURL2 = canvas.toDataURL(); if (dataURL1 !== dataURL2){ log("data URL missmatch:", dataURL1, "!=", dataURL2 ); return true; } return false; }); addTest("readout - in - out test", function(log){ "use strict"; const canvas = document.createElement("canvas"); const context = canvas.getContext("2d"); const imageData = context.getImageData(0, 0, canvas.width, canvas.height); for (let i = 0; i < imageData.data.length; i += 1){ if (i % 4 !== 3){ imageData.data[i] = Math.floor(256 * Math.random()); } else { imageData.data[i] = 255; } } context.putImageData(imageData, 0, 0); const imageData1 = context.getImageData(0, 0, canvas.width, canvas.height); const canvas2 = document.createElement("canvas"); const context2 = canvas2.getContext("2d"); context2.putImageData(imageData1, 0, 0); const imageData2 = context2.getImageData(0, 0, canvas.width, canvas.height); for (let i = 0; i < imageData2.data.length; i += 1){ if (imageData1.data[i] !== imageData2.data[i]){ log("mismatch at", i, ":", imageData1.data[i], "(", imageData1.data[i].toString(2), ")", "!=", imageData2.data[i], "(", imageData2.data[i].toString(2), ")" ); return true; } } return false; }); addTest("window name change", function(log){ "use strict"; const oldName = window.name; log("old name:", oldName); const newName = oldName + " added"; log("new name:", newName); window.name = newName; if (window.name !== newName){ log("window name not set:", window.name); return true; } return false; }); function checkDOMRectData(rect, data, log){ "use strict"; let detected = false; ["x", "y", "width", "height"].forEach(function(property){ if (data[property] !== rect[property]){ log("Wrong value for", property, ":", data[property], "!=", rect[property]); detected = true; } }); return detected; } function getRectByData(data){ "use strict"; const el = document.createElement("div"); el.style.cssText = "position: fixed;" + "left: " + data.x + "px; " + "top: " + data.y + "px; " + "width: " + data.width + "px; " + "height: " + data.height + "px;"; document.body.appendChild(el); const rect = el.getBoundingClientRect(); document.body.removeChild(el); return rect; } addTest("self created DOMRect", function(log){ "use strict"; const data = { x: Math.PI, y: Math.E, width: Math.LOG10E, height: Math.LOG2E }; const rect = new DOMRect(data.x, data.y, data.width, data.height); return checkDOMRectData(rect, data, log); }); addTest("known DOMRect", function(log){ "use strict"; const data = { x: 1 + 1/4, y: 2, width: 3, height: 4 }; const rect = getRectByData(data); return checkDOMRectData(rect, data, log); }); addTest("changed DOMRect", function(log){ "use strict"; const data = { x: Math.PI, y: 2, width: 3, height: 4 }; const rect = getRectByData(data); rect.x = Math.PI; return checkDOMRectData(rect, data, log); }); addTest("recreated DOMRect", function(log){ "use strict"; const data = { x: Math.PI, y: Math.E, width: Math.LOG10E, height: Math.LOG2E }; const rect = getRectByData(data); const rect2 = getRectByData(rect); return checkDOMRectData(rect2, rect, log); });