Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Front-End Performance: The Dark Side @ ColdFron...

Mathias Bynens
September 01, 2016

Front-End Performance: The Dark Side @ ColdFront Conference 2016

In security-sensitive situations, performance can actually be a bug rather than a feature. This presentation covers timing attacks on the web, and demonstrates how modern performance-related web APIs can sometimes have a negative security impact.

More information: https://dev.opera.com/blog/timing-attacks/

Mathias Bynens

September 01, 2016
Tweet

More Decks by Mathias Bynens

Other Decks in Technology

Transcript

  1. @mathias function compare(a, b) { return a === b; }

    compare('Fronteers', 'Fronteers'); // → true @ 1000 μs compare('Fronteers', 'Fronteerz'); // → false @ 1000 μs compare('Spring', 'Thing'); // → false @ 100 μs compare('Spring', 'Zpring'); // → false @ 200 μs
  2. @mathias function compare(a, b) { return a === b; }

    compare('ColdFront', 'ColdFront'); // → true @ 1000 μs compare('ColdFront', 'ColdFrond'); // → false @ 1000 μs compare('Pikachu', 'Pichu'); // → false @ 100 μs compare('CSS', 'XSS'); // → false @ 200 μs
  3. @mathias function compare(a, b) { return a === b; }

    compare('ColdFront', 'ColdFront'); // → true @ 1000 μs compare('ColdFront', 'ColdFrond'); // → false @ 1000 μs compare('Pikachu', 'Pichu'); // → false @ 100 μs compare('CSS', 'XSS'); // → false @ 200 μs
  4. @mathias function compare(a, b) { const lengthA = a.length; if

    (lengthA !== b.length) { return false; // performance optimization #1 } for (let index = 0; index < lengthA; index++) { if (a.charCodeAt(index) !== b.charCodeAt(index)) { return false; // performance optimization #2 } } return true; // worst-case perf scenario }
  5. @mathias compare('ColdFront', 'ColdFront'); // → true @ 1000 μs compare('ColdFront',

    'ColdFrond'); // → false @ 1000 μs [opt. #2] compare('Pikachu', 'Pichu'); // → false @ 100 μs [opt. #1] compare('CSS', 'XSS'); // → false @ 200 μs [opt. #2]
  6. @mathias function compare(a, b) { const lengthA = a.length; if

    (lengthA !== b.length) { return false; // performance optimization #1 // allows attackers to figure out expected length } for (let index = 0; index < lengthA; index++) { if (a.charCodeAt(index) !== b.charCodeAt(index)) { return false; // performance optimization #2 // allows attackers to figure out expected // characters, one by one (except the last one) } } return true; // worst-case perf scenario }
  7. @mathias function safeCompare(a, b) { const lengthA = a.length; let

    result = 0; if (lengthA !== b.length) { b = a; result = 1; } for (let index = 0; index < lengthA; index++) { result |= ( a.charCodeAt(index) ^ b.charCodeAt(index) ); // XOR } return result === 0; }
  8. @mathias const links = document.querySelector(':visited'); for (const link of links)

    { console.log(`The user has visited ${ link.href }!`); }
  9. @mathias const links = document.querySelector(':visited'); for (const link of links)

    { console.log(`The user has visited ${ link.href }!`); } not a timing attack
  10. @mathias for (const link of document.links) { const color =

    getComputedStyle(link).color; if (color === 'rgb(255, 0, 0)') { // The color is red, i.e. `:visited` applies. console.log(`The user has visited ${ link.href }!`); } }
  11. @mathias for (const link of document.links) { const color =

    getComputedStyle(link).color; if (color === 'rgb(255, 0, 0)') { // The color is red, i.e. `:visited` applies. console.log(`The user has visited ${ link.href }!`); } } not a timing attack
  12. @mathias /* CSS */ :link { /* Increasing blur-radius makes

    (re-)rendering */ /* links slower. */ text-shadow: 100px 100px 199px #000; } /* JavaScript */ requestAnimationFrame(timeEachFrame);
  13. @mathias /* CSS */ :link { /* Increasing blur-radius makes

    (re-)rendering */ /* links slower. */ text-shadow: 100px 100px 199px #000; } /* JavaScript */ requestAnimationFrame(timeEachFrame); timing attack!!1
  14. @mathias const image = new Image(); image.onerror = stopTimer; const

    end = performance.now(); const delta = end - start; alert(`Loading took ${ delta } milliseconds.`); }; startTimer(); image.src = 'https://example.com/admin.php';
  15. @mathias const image = new Image(); image.onerror = function() {

    const end = performance.now(); const delta = end - start; alert(`Loading took ${ delta } milliseconds.`); }; const start = performance.now(); image.src = 'https://example.com/admin.php';
  16. @mathias const image = new Image(); image.onerror = function() {

    const end = performance.now(); const delta = end - start; alert(`Loading took ${ delta } milliseconds.`); }; const start = performance.now(); image.src = 'https://example.com/admin.php';
  17. @mathias const start = performance.now(); fetch(url, { 'credentials': 'include', 'mode':

    'no-cors' }).then(function() { const entries = performance.getEntriesByName(url); const fetchStart = entries.pop().fetchStart; const delta = fetchStart - start; const isRedirect = delta > 10; if (isRedirect) { console.log(`The URL ${ url } is a redirect.`); } else { console.log(`The URL ${ url } is not a redirect.`); } });
  18. @mathias const start = performance.now(); fetch(url, { 'credentials': 'include', 'mode':

    'no-cors' }).then(function() { const entries = performance.getEntriesByName(url); const fetchStart = entries.pop().fetchStart; const delta = fetchStart - start; const isRedirect = delta > 10; if (isRedirect) { console.log(`The URL ${ url } is a redirect.`); } else { console.log(`The URL ${ url } is not a redirect.`); } });
  19. @mathias const start = performance.now(); fetch(url, { 'credentials': 'include', 'mode':

    'no-cors' }).then(function() { const entries = performance.getEntriesByName(url); const fetchStart = entries.pop().fetchStart; const delta = fetchStart - start; const isRedirect = delta > 10; if (isRedirect) { console.log(`The URL ${ url } is a redirect.`); } else { console.log(`The URL ${ url } is not a redirect.`); } });
  20. @mathias const start = performance.now(); fetch(url, { 'credentials': 'include', 'mode':

    'no-cors' }).then(function() { const entries = performance.getEntriesByName(url); const fetchStart = entries.pop().fetchStart; const delta = fetchStart - start; const isRedirect = delta > 10; if (isRedirect) { console.log(`The URL ${ url } is a redirect.`); } else { console.log(`The URL ${ url } is not a redirect.`); } });
  21. @mathias const start = performance.now(); fetch(url, { 'credentials': 'include', 'mode':

    'no-cors' }).then(function() { const entries = performance.getEntriesByName(url); const fetchStart = entries.pop().fetchStart; const delta = fetchStart - start; const isRedirect = delta > 10; if (isRedirect) { console.log(`The URL ${ url } is a redirect.`); } else { console.log(`The URL ${ url } is not a redirect.`); } });
  22. @mathias const video = document.createElement('video'); // `suspend` event == download

    complete video.onsuspend = startTimer; // `error` event == parsing complete video.onerror = stopTimer; video.src = 'https://example.com/admin.php';
  23. @mathias const url = 'https://example.com/admin.php'; const dummyRequest = new Request('dummy');

    fetch(url, { 'credentials': 'include', 'mode': 'no-cors' }).then(function(response) { // The download has completed. startTimer(); return cache.put(dummyRequest, response.clone()); }).then(function() { // The resource has been stored in the cache. stopTimer(); });
  24. @mathias “HEIST is a set of techniques that exploit timing

    side-channels in the browser […] to determine whether a response fitted into a single TCP window or whether it needed multiple. […] an attacker can determine the exact amount of bytes that were needed to send the response back to the client, all from within the browser. It so happens to be that knowing the exact size of a cross-origin resource is just what you need to launch a compression-based attack, which can be used to extract content (e.g. CSRF tokens) from any website using gzip compression.”
  25. @mathias “HEIST is a set of techniques that exploit timing

    side-channels in the browser […] to determine whether a response fitted into a single TCP window or whether it needed multiple. […] an attacker can determine the exact amount of bytes that were needed to send the response back to the client, all from within the browser. It so happens to be that knowing the exact size of a cross-origin resource is just what you need to launch a compression-based attack, which can be used to extract content (e.g. CSRF tokens) from any website using gzip compression.”
  26. @mathias THANKS! Research by @pdjstone: mths.be/bvn Sniffly by @bcrypt: mths.be/buy

    Research by @tomvangoethem: mths.be/buz HEIST by @tomvangoethem & @vanhoefm: mths.be/bvp Introduction to Same-Site cookies: mths.be/bvq