A Controversial Codewars Exploit: Mocking require.cache to Pass Any Kata



There is a class of "solution" to Codewars katas that involves not solving the kata. You submit some code, the green ticks appear, and the platform congratulates you on completing a problem you didn't read. The trick has nothing to do with the kata itself. It's about the test harness that runs your code, and a peculiarity of how Node.js loads modules.
What follows is a thought experiment. I am not suggesting anyone actually do this — Codewars exists for the practice, and gaming your way through the rankings is the cognitive equivalent of doing pushups by lying on the floor and imagining them. But the mechanism underneath is genuinely interesting, and it generalises to a lot of places where you would rather it didn't.
Node's require cache
Every time you require a module in Node, the loaded module gets dropped into an object called require.cache. The next require of the same path doesn't re-evaluate the file — it just returns the cached export. This is why side effects in a top-level module file only ever run once per process.
The cache is not hidden. It is sitting on the require function itself, and you can poke at it like any other object:
console.log(require.cache);
You get a four-thousand-line wall of key-value pairs. Each key is an absolute path to a loaded file. Each value is the module record — including, crucially, its exports.
On a Codewars Node kata, the harness has already loaded chai by the time your code runs. Which means somewhere in that cache there is an entry whose key ends in chai/index.js, and whose exports.assert is the live assertion library the tests are about to call against your output.
The exploit, in three lines
Find the chai entry. Reach into its exports. Replace the assertion function with one that returns true regardless of what's passed in.
for (const [key, value] of Object.entries(require.cache)) {
if (key.endsWith('chai/index.js')) {
value.exports.assert.strictEqual = () => true;
}
}
When the test suite then runs assert.strictEqual(yourAnswer, expected), it isn't calling chai's strict-equality check anymore. It is calling a function that returns true and goes home.
Run the tests. They pass.
The wrinkle: attempt vs test
There is one small problem. The "test" button on Codewars and the "attempt" button run subtly different code paths, and on the attempt run your modification disappears. The reason is module load order: on attempt, your code is evaluated before chai has been pulled into the require cache. The loop runs against a cache that doesn't yet contain the module you want to mutate, finds nothing, mutates nothing, and the real assertions fire on a real solution that doesn't exist.
The fix is appropriately ugly:
setTimeout(() => {
for (const [key, value] of Object.entries(require.cache)) {
if (key.endsWith('chai/index.js')) {
value.exports.assert.strictEqual = () => true;
}
}
}, 1);
A one-millisecond timeout. By the time the callback fires, the harness has finished its setup, chai is in the cache, and the override lands on the live exports a beat before the assertions run. You can now paste that block into any chai-based kata — easy, hard, "extreme" — and the green tick arrives without you reading the problem statement.
I tried it on one of the harder katas to prove the point, watched it pass, and didn't hit submit. Rank-farming by exploit is depressing in a way I can't quite articulate.
Why this is allowed to work
Nothing about this is a Node bug. The require cache is documented, public, and writeable for good reason — it's how the entire mocking ecosystem functions. Jest, Sinon, proxyquire, mock-require: they all work by reaching into the cache and replacing module exports with test doubles before the code under test pulls them in. The technique above is the same technique, pointed in the opposite direction. Instead of replacing the thing being tested with a stub, it replaces the thing doing the testing with a stub.
This is the part worth dwelling on. Any test runner that lives in the same process as the code it's testing has, by definition, no privacy from that code. The runner's internals — its assertion functions, its result reporters, its scheduling — are sitting in memory the user code can touch. If a sandbox boundary is "we both share a JavaScript heap," it is not a boundary. It is a polite request.
Where this generalises
Codewars is a toy. The pattern is not.
- CI runners that execute untrusted contributor code in the same process as the test harness. If a fork's PR can run code on your CI, that code can mutate your reporters and lie about the result. Container isolation is what saves you, not "tests passed."
- Online code playgrounds that grade student submissions in a shared Node instance. Same problem, different audience.
- Plugin systems that load third-party modules into the host process and trust them to behave. Plugins can rewrite the host's exports for every other plugin and the host itself.
- Anything labelled "sandbox" that turns out, on inspection, to be a
vm.runInContextwith a casually-passed-in global.vmis not a security boundary either, and the Node docs are explicit about it.
The general rule is dull and worth tattooing somewhere: if the adversary's code shares an address space with your code, the adversary's code wins. The only real isolation is a process boundary, and ideally a container or VM around that.
Codewars almost certainly knows about this. The fix isn't to make the require cache read-only — half of Node tooling would stop working overnight. The fix is to grade submissions in a separate, locked-down process where the harness reads your code's output rather than running alongside it. Most serious code-execution services do exactly that. Codewars is, I suspect, optimising for "people complete katas and feel good," which is a perfectly reasonable place to land on the security/usability axis when the worst case is someone lying to themselves on a leaderboard.
Still. It's a nice little window into the machinery. The next time you write jest.mock('./thing') and it magically replaces a transitive dependency three layers deep, you now know exactly which object it's reaching into to do it.