2

I noticed today that I can replace a sensitive built-in JS function like this:

async function _hackedEncrypt(algorithm, key, data) {
   console.log('hacked you!');
}

const subtle = global.crypto.subtle; // Assign to get around "read-only" error.
subtle.encrypt = _hackedEncrypt;

global.crypto.subtle.encrypt(); // 'Hacked you!' appears in console.

Yikes!

This exploit is so simple. Any of the thousands of dependencies (direct and transitive) in my web app could make this function reassignment. Note that my question isn't specific to Web Crypto - it's just one of the more dangerous targets for an attacker.

How can I either detect that the function has been reassigned or guarantee that I'm always calling the original browser implementation of it?

Erik Hermansen
  • 2,200
  • 3
  • 21
  • 41
  • Running one virus compromises your entire system. Don't have thousands of dependencies. – ASDFGerte May 31 '22 at 20:13
  • An empty `create-react-app` comes with 1380 dependencies before you add one line of code to it. So with my chosen platform, a very popular one, I can't take your advice. – Erik Hermansen May 31 '22 at 20:28
  • I am writing react, i've never used `create-react-app`. `react` itself (since a recent commit) will have zero dependencies, `react-dom` still has two. – ASDFGerte May 31 '22 at 20:30
  • Your point has merit. I am still looking for a solution that doesn't depend on trusting my direct and transitive dependencies to not expose me to this exploit. – Erik Hermansen May 31 '22 at 20:42
  • Especially with node, e.g. what you run in `create-react-app`, you are executing code, which has permissions to read/write/execute on your drive. It can do basically anything, and is almost never sandboxed in any way. There are plenty of examples, where this went wrong, [here is a recent one that comes to mind](https://snyk.io/blog/peacenotwar-malicious-npm-node-ipc-package-vulnerability/), note how recent, and how many were affected. – ASDFGerte May 31 '22 at 20:47
  • 1
    What you're referring to is a supply chain attack. If you have a compromised dependency, it can siphon users credit cards etc. without replacing built-in JS functions, by just registering callbacks. There are [dedicated security products for protecting against supply chain attacks](https://cybersecurity-excellence-awards.com/candidate-category/security-product-awards-category/client-side-security/). – root Jun 04 '22 at 06:53

3 Answers3

1

What you are describing is a subset of a broader class of attacks: Supply Chain Attacks.

The basic idea is that I create a JS library that is useful and trivial, over time a ton of projects depend on it, then I insert a back door, next time they upgrade they're dependencies - I have access to their users' browsers.
I can then steal passwords, credit card info, PII, etc. - basically digital skimming. Magecart is probably being the best known attack in the JS space. Probably the best known one in general is the Solarwinds attack.

One solution is to roll your own. That is pretty much hopeless, because even though you might know what APIs you use, you don't know what your dependencies use, and how that changes.

Another solution is Content Security Policy, which controls outgoing requests: a skimmer might have been able to put their code in your app, but they can't do anything useful if your content security policy doesn't let it issue requests to unknown hosts.

There are also commercial solution. I'm aware of:

root
  • 5,528
  • 1
  • 7
  • 15
0

In my index.js, I import this module ahead of any other modules. It must be the first import so that other dependencies don't get a chance to replace a function before I can set a reference to the original.

const originalEncrypt = global.crypto.subtle.encrypt;
// Any number of functions worth protecting can be added here
// and also added to the check in wereFunctionsReplaced().

function wereFunctionsReplaced() {
  return global.crypto.subtle.encrypt !== originalEncrypt;
}

...and then I call wereFunctionsReplaced() ahead of the function call I'm worried about:

if (wereFunctionsReplaced()) throw Error('Everybody run to the panic room!');
global.crypto.subtle.encrypt(algorithm, key, data);

Of course, I could also wrap the protected functions like:

function safeEncrypt(algorithm, key, data) {
  if (wereFunctionsReplaced()) throw Error('Everybody run to the panic room!');
  return global.crypto.subtle.encrypt(algorithm, key, data);
}

In some cases, it would be more convenient to store the built-in function to a variable and call the function directly from the variable rather than from global.*. But for crypto.subtle.encrypt it will throw a "Bad invocation" error in Chrome. And thinking harder about it, many subtle, unexpected behaviors could be caused by calling the built-in functions outside their expected location in the global hierarchy.

This is a self-answer for posterity. But I would love to hear better solutions than this one. Criticism on any faults this solution may have is welcome as well.

Erik Hermansen
  • 2,200
  • 3
  • 21
  • 41
  • Con: The `wereFunctionsReplaced()` function can itself be replaced. It is much less likely to be targeted in an attack launched from a 3rd-party dependency, however. – Erik Hermansen May 31 '22 at 22:43
  • 1
    If you want security by obscurity, where you differ from the norm far enough, so non-adapted malware is likely to stumble on your code, you may want to look into [`Object.freeze`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze), and the [`configurable`/`writable` attributes of property descriptors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty). Note again, that i do not believe anything will protect you against malware adapted to your case - there is always some loophole to abuse. – ASDFGerte Jun 01 '22 at 08:44
0

There is finally a solution to your problem in the form of a consumable js lib.

After long research and development in the supply chain attack field, I created Securely to answer this problem exactly:

After loading securely in the web page, the SECURE API will be exposed (make sure this runs first - anything that runs before securely can disable its effect).

First you declare what APIs you want to protect:

const securely = SECURE(window, {
    objects: {
        'window': ['fetch'],
    }
});

Then you use the returned securely callback to run secured operations and by that have access to the native original function, even if it was overridden:

window.fetch = () => console.log('DISABLED');

securely(_ => {
  window.fetchS('https://example.com/sensitive_request/');
});

window.fetch('https://example.com/sensitive_request/'); // DISABLED

In the example, the fetchS property is inaccessible unless using the securely callback (this is to hide the real behaviour from malicious entities in the webpage).

Read more about it, help with the project is highly appreciated!

Here's a demonstration of how it works live.

Securely is part of a broader attempt to better defend against supply chain attacks. To learn more about our effort, check out Snow, Across which are all part of the LavaMoat project.