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 full-size slide

  2. @mathias
    THE DARK SIDE
    FRONT-END PERFORMANCE

    View full-size slide

  3. @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 full-size slide

  4. @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 full-size 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 full-size slide

  6. @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 full-size slide

  7. @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 full-size slide

  8. @mathias
    SIDE-CHANNEL LEAK

    View full-size slide

  9. @mathias
    TIMING ATTACK

    View full-size slide

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

    View full-size slide

  11. @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 full-size slide

  12. @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 full-size slide

  13. @mathias
    STEALING BROWSING HISTORY

    View full-size slide

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

    View full-size slide

  15. @mathias

    Facebook
    Twitter
    Reddit
    Instagram
    GitHub

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  18. @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 full-size 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 }!`);
    }
    }
    not a timing attack

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  22. @mathias
    SNIFFLY
    @BCRYPT

    View full-size slide

  23. @mathias
    SIMPLEST POSSIBLE TIMING ATTACK ON THE WEB

    View full-size slide

  24. @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 full-size slide

  25. @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 full-size slide

  26. @mathias
    750 ms

    View full-size slide

  27. @mathias
    1250 ms

    View full-size slide

  28. @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 full-size slide

  29. @mathias
    REDIRECT DETECTION ⏱ ATTACK
    @TOMVANGOETHEM

    View full-size slide

  30. @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 full-size slide

  31. @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 full-size slide

  32. @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 full-size slide

  33. @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 full-size 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 full-size slide

  35. @mathias
    VIDEO PARSING ⏱ ATTACK
    @TOMVANGOETHEM

    View full-size slide

  36. @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 full-size slide

  37. @mathias
    CACHE STORAGE ⏱ ATTACK
    @TOMVANGOETHEM

    View full-size slide

  38. @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 full-size slide

  39. @mathias
    30 ms

    View full-size slide

  40. @mathias
    15 ms

    View full-size slide

  41. @mathias
    HEIST

    View full-size slide

  42. @mathias
    HTTP

    View full-size slide

  43. @mathias
    HTTP
    Encrypted

    View full-size slide

  44. @mathias
    HTTP
    Encrypted
    Information can be

    View full-size slide

  45. @mathias
    HTTP
    Encrypted
    Information can be
    Stolen through

    View full-size slide

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

    View full-size slide

  47. @mathias
    mths.be/bvo

    View full-size slide

  48. @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 full-size slide

  49. @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 full-size slide

  50. @mathias
    PREVENTION

    View full-size slide

  51. @mathias
    SAME-SITE COOKIES

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  55. @mathias
    BLOCK THIRD-PARTY ###

    View full-size slide

  56. @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 full-size slide