CVE-2025-59049
Description
Mockoon provides way to design and run mock APIs. Prior to version 9.2.0, a mock API configuration for static file serving follows the same approach presented in the documentation page, where the server filename is generated via templating features from user input is vulnerable to Path Traversal and LFI, allowing an attacker to get any file in the mock server filesystem. The issue may be particularly relevant in cloud hosted server instances. Version 9.2.0 fixes the issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@mockoon/commons-servernpm | < 9.2.0 | 9.2.0 |
@mockoon/clinpm | < 9.2.0 | 9.2.0 |
Affected products
1Patches
28c7c42674635Fix snap options in electron builder config
1 file changed · +3 −3
packages/desktop/build-configs/electron-builder.linux.js+3 −3 modified@@ -31,10 +31,10 @@ const config = Object.assign({}, commonConfig, { Name: 'Mockoon', Type: 'Application', Categories: 'Development' - }, - snap: { - base: 'core22' } + }, + snap: { + base: 'core22' } });
c7f6e23e87dcFix path traversal vulnerability in file paths
6 files changed · +194 −67
packages/commons-server/src/libs/server/server.ts+86 −50 modified@@ -43,7 +43,7 @@ import { } from 'https'; import killable from 'killable'; import { lookup as mimeTypeLookup } from 'mime-types'; -import { basename, extname } from 'path'; +import { basename, extname, isAbsolute, resolve } from 'path'; import { match } from 'path-to-regexp'; import { parse as qsParse } from 'qs'; import rangeParser from 'range-parser'; @@ -97,6 +97,7 @@ export class MockoonServer extends (EventEmitter as new () => TypedEmitter<Serve // templating global variables private globalVariables: Record<string, any> = {}; private options: ServerOptions = { + environmentDirectory: '.', disabledRoutes: [], envVarsPrefix: defaultEnvironmentVariablesPrefix, enableAdminApi: true, @@ -873,25 +874,9 @@ export class MockoonServer extends (EventEmitter as new () => TypedEmitter<Serve enabledRouteResponse.bodyType === BodyTypes.FILE && enabledRouteResponse.filePath ) { - const templateParser = (contentData: string) => - TemplateParser({ - shouldOmitDataHelper: false, - content: contentData, - environment: this.environment, - processedDatabuckets: this.processedDatabuckets, - globalVariables: this.globalVariables, - request: finalRequest, - envVarsPrefix: this.options.envVarsPrefix - }); - - // resolve file location - let filePath = templateParser( - // replace backslashes with forward slashes, but not if followed by a dot (to allow helpers with paths containing properties with dots: e.g. {{queryParam 'path.prop\.with\.dots'}}) - enabledRouteResponse.filePath.replace(/\\(?!\.)/g, '/') - ); - filePath = resolvePathFromEnvironment( - filePath, - this.options.environmentDirectory + const filePath = this.getSafeFilePath( + enabledRouteResponse.filePath, + finalRequest ); serveFileContentInWs( @@ -900,7 +885,16 @@ export class MockoonServer extends (EventEmitter as new () => TypedEmitter<Serve enabledRouteResponse, this, filePath, - templateParser + (contentData: string) => + TemplateParser({ + shouldOmitDataHelper: false, + content: contentData, + environment: this.environment, + processedDatabuckets: this.processedDatabuckets, + globalVariables: this.globalVariables, + request: finalRequest, + envVarsPrefix: this.options.envVarsPrefix + }) ); return; @@ -1428,21 +1422,7 @@ export class MockoonServer extends (EventEmitter as new () => TypedEmitter<Serve envVarsPrefix: this.options.envVarsPrefix }); - let filePath = TemplateParser({ - shouldOmitDataHelper: false, - // replace backslashes with forward slashes, but not if followed by a dot (to allow helpers with paths containing properties with dots: e.g. {{queryParam 'path.prop\.with\.dots'}}) - content: callback.filePath.replace(/\\(?!\.)/g, '/'), - environment: this.environment, - processedDatabuckets: this.processedDatabuckets, - globalVariables: this.globalVariables, - request: serverRequest, - envVarsPrefix: this.options.envVarsPrefix - }); - - filePath = resolvePathFromEnvironment( - filePath, - this.options.environmentDirectory - ); + const filePath = this.getSafeFilePath(callback.filePath, serverRequest); const fileMimeType = mimeTypeLookup(filePath) || ''; @@ -1579,20 +1559,9 @@ export class MockoonServer extends (EventEmitter as new () => TypedEmitter<Serve const serverRequest = fromExpressRequest(request); try { - let filePath = TemplateParser({ - shouldOmitDataHelper: false, - // replace backslashes with forward slashes, but not if followed by a dot (to allow helpers with paths containing properties with dots: e.g. {{queryParam 'path.prop\.with\.dots'}}) - content: routeResponse.filePath.replace(/\\(?!\.)/g, '/'), - environment: this.environment, - processedDatabuckets: this.processedDatabuckets, - globalVariables: this.globalVariables, - request: serverRequest, - envVarsPrefix: this.options.envVarsPrefix - }); - - filePath = resolvePathFromEnvironment( - filePath, - this.options.environmentDirectory + const filePath = this.getSafeFilePath( + routeResponse.filePath, + serverRequest ); const fileMimeType = mimeTypeLookup(filePath) || ''; @@ -2433,4 +2402,71 @@ export class MockoonServer extends (EventEmitter as new () => TypedEmitter<Serve response.status(response.locals.statusCode); } } + + /** + * Parse file paths and prevent path traversal + * + * If the path is absolute, it must stay within its original static base + * (before the first {{...}}) + * If the path is relative, it must stay within the environment base directory + * + * @param filePath + * @param request + * @returns + */ + private getSafeFilePath(filePath: string, request?: ServerRequest) { + const resolvePath = (path: string) => { + const isPathAbsolute = isAbsolute(path); + + return isPathAbsolute + ? resolve(path) + : resolve(this.options.environmentDirectory, path); + }; + // Convert backslashes to forward slashes (Windows compatibility) + const rawFilePath = filePath.replace(/\\(?!\.)/g, '/'); + + // Check if there is any templating helper in the file path + const hasTemplatingHelper = /{{2,3}[^}]+}{2,3}/.test(rawFilePath); + + if (!hasTemplatingHelper) { + // If no templating helper, allow unrestricted access + return resolvePath(rawFilePath); + } + + // Extract static base from templated string (before first {{...}}) + const staticBaseMatch = rawFilePath.match(/^([^{}]+)/); + const staticBaseDir = staticBaseMatch ? resolve(staticBaseMatch[1]) : null; + + const parsedFilePath = TemplateParser({ + shouldOmitDataHelper: false, + content: rawFilePath, + environment: this.environment, + processedDatabuckets: this.processedDatabuckets, + globalVariables: this.globalVariables, + request, + envVarsPrefix: this.options.envVarsPrefix + }); + + // Determine if the path is absolute or relative + const isPathAbsolute = isAbsolute(parsedFilePath); + const resolvedPath = resolvePath(parsedFilePath); + + if (isPathAbsolute) { + // Absolute paths must stay within their original static base + if (!staticBaseDir || !resolvedPath.startsWith(staticBaseDir)) { + throw new Error( + `Access to absolute path outside of the original static base directory (${resolvedPath})` + ); + } + } else { + // Relative paths must stay within the environment base directory + if (!resolvedPath.startsWith(this.options.environmentDirectory)) { + throw new Error( + `Access to relative path outside of the environment base directory (${resolvedPath})` + ); + } + } + + return resolvedPath; + } }
packages/commons-server/test/data/environments/test-env.json+35 −13 modified@@ -1,6 +1,6 @@ { "uuid": "c6199444-5116-490a-99a2-074876253a4a", - "lastMigration": 32, + "lastMigration": 33, "name": "Test env", "port": 3000, "hostname": "", @@ -34,7 +34,9 @@ } ], "responseMode": null, - "type": "http" + "type": "http", + "streamingMode": null, + "streamingInterval": 0 }, { "uuid": "1d4dff08-def4-41eb-bebd-d6f3c670618e", @@ -68,7 +70,9 @@ "callbacks": [] } ], - "responseMode": null + "responseMode": null, + "streamingMode": null, + "streamingInterval": 0 }, { "uuid": "a8a4e784-4fdf-497f-8915-774c4aa70205", @@ -85,7 +89,7 @@ "label": "", "headers": [], "bodyType": "FILE", - "filePath": "./test/data/test.data", + "filePath": "../test.data", "databucketID": "", "sendFileAsBody": true, "rules": [], @@ -97,7 +101,9 @@ "callbacks": [] } ], - "responseMode": null + "responseMode": null, + "streamingMode": null, + "streamingInterval": 0 }, { "uuid": "bd03d74d-ba12-47b2-acf2-1cd8093e7e66", @@ -126,7 +132,9 @@ "callbacks": [] } ], - "responseMode": null + "responseMode": null, + "streamingMode": null, + "streamingInterval": 0 }, { "uuid": "f43181bb-40f4-49e6-a886-1151f3cdb684", @@ -160,7 +168,9 @@ } ], "responseMode": null, - "type": "http" + "type": "http", + "streamingMode": null, + "streamingInterval": 0 }, { "uuid": "290fe1d2-a924-4dd5-b6c0-d36190f990f8", @@ -194,7 +204,9 @@ } ], "responseMode": null, - "type": "http" + "type": "http", + "streamingMode": null, + "streamingInterval": 0 }, { "uuid": "6c82f28e-f2c0-4752-a081-6e1bec5fd6ee", @@ -228,7 +240,9 @@ } ], "responseMode": null, - "type": "http" + "type": "http", + "streamingMode": null, + "streamingInterval": 0 }, { "uuid": "cab652d8-ca19-4d1d-9ab4-81c0f954d8fa", @@ -262,7 +276,9 @@ } ], "responseMode": null, - "type": "http" + "type": "http", + "streamingMode": null, + "streamingInterval": 0 }, { "uuid": "8d332421-d48f-4561-8100-a7fc2cc43828", @@ -296,7 +312,9 @@ } ], "responseMode": null, - "type": "http" + "type": "http", + "streamingMode": null, + "streamingInterval": 0 }, { "uuid": "c406bb7c-2e37-4c8c-9a18-ae6381ddb9f3", @@ -330,7 +348,9 @@ } ], "responseMode": null, - "type": "http" + "type": "http", + "streamingMode": null, + "streamingInterval": 0 }, { "uuid": "3485b92c-4ce4-4921-b9f3-9e0eaa8ba867", @@ -359,7 +379,9 @@ "callbacks": [] } ], - "responseMode": null + "responseMode": null, + "streamingMode": null, + "streamingInterval": 0 } ], "proxyMode": false,
packages/commons-server/test/specs/server/range-header.test.ts+4 −1 modified@@ -1,5 +1,6 @@ import { Environment } from '@mockoon/commons'; import { strictEqual } from 'node:assert'; +import { resolve as pathResolve } from 'node:path'; import { after, before, describe, it } from 'node:test'; import { MockoonServer } from '../../../src'; import { getEnvironment } from '../../libs/environment'; @@ -12,7 +13,9 @@ describe('Range headers', () => { environment = await getEnvironment('test'); environment.port = 3010; - server = new MockoonServer(environment); + server = new MockoonServer(environment, { + environmentDirectory: pathResolve('./test/data/environments/') + }); await new Promise((resolve, reject) => { server.on('started', () => {
packages/commons/src/models/server.model.ts+1 −1 modified@@ -89,7 +89,7 @@ export type ServerOptions = { /** * Directory where to find the environment file. */ - environmentDirectory?: string; + environmentDirectory: string; /** * List of routes uuids to disable.
packages/desktop/test/specs/file.spec.ts+67 −1 modified@@ -11,12 +11,78 @@ describe('File serving', () => { await environments.open('basic-data'); }); + describe('Path escape', () => { + it('should allow escaping when there is no templating', async () => { + await environments.start(); + await routes.select(2); + await routes.selectBodyType(BodyTypes.FILE); + await routes.setFile('../window-state.json'); + + await utils.waitForAutosave(); + + await http.assertCall({ + // this file always exists in the data folder + path: '/answer', + method: 'GET', + testedResponse: { + status: 200, + body: { + contains: 'isFullScreen' + } + } + }); + }); + + it('should return an error when trying to escape a relative path', async () => { + await routes.select(2); + await routes.selectBodyType(BodyTypes.FILE); + await routes.setFile("./{{queryParam 'filename'}}"); + + await utils.waitForAutosave(); + + await http.assertCall({ + // this file always exists in the data folder + path: '/answer?filename=../window-state.json', + method: 'GET', + testedResponse: { + status: 200, + body: { + contains: + 'Error while serving the content: Access to relative path outside of the environment base directory' + } + } + }); + }); + + it('should return an error when trying to escape an absolute path', async () => { + await routes.select(2); + await routes.selectBodyType(BodyTypes.FILE); + await routes.setFile( + `${process.cwd()}/tmp/storage/{{queryParam 'filename'}}` + ); + await utils.waitForAutosave(); + + await http.assertCall({ + // this file always exists in the data folder + path: '/answer?filename=../window-state.json', + method: 'GET', + testedResponse: { + status: 200, + body: { + contains: + 'Error while serving the content: Access to absolute path outside of the original static base directory' + } + } + }); + }); + }); + describe('File not found', () => { it('should return an error and keep the defined status', async () => { await routes.select(2); await routes.selectBodyType(BodyTypes.FILE); await routes.setFile('./non-existing-file.txt'); - await environments.start(); + await utils.waitForAutosave(); await http.assertCall({ path: '/answer',
packages/serverless/src/libs/serverless.ts+1 −1 modified@@ -13,7 +13,7 @@ import { RequestListener } from 'http'; import ServerlessHttp from 'serverless-http'; export class MockoonServerless { - private options: ServerOptions & { logTransaction: boolean } = { + private options: Partial<ServerOptions> & { logTransaction: boolean } = { logTransaction: false, disabledRoutes: [], fakerOptions: {},
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-w7f9-wqc4-3wxrghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-59049ghsaADVISORY
- github.com/mockoon/mockoon/blob/1ed31c4059d7f757f6cb2a43e10dc81b0d9c55a9/packages/commons-server/src/libs/server/server.tsnvdWEB
- github.com/mockoon/mockoon/blob/1ed31c4059d7f757f6cb2a43e10dc81b0d9c55a9/packages/commons-server/src/libs/server/server.tsnvdWEB
- github.com/mockoon/mockoon/commit/c7f6e23e87dc3b8cc44e5802af046200a797bd2envdWEB
- github.com/mockoon/mockoon/security/advisories/GHSA-w7f9-wqc4-3wxrnvdWEB
News mentions
0No linked articles in our index yet.