VYPR
High severity7.5OSV Advisory· Published Sep 10, 2025· Updated Apr 15, 2026

CVE-2025-59049

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.

PackageAffected versionsPatched versions
@mockoon/commons-servernpm
< 9.2.09.2.0
@mockoon/clinpm
< 9.2.09.2.0

Affected products

1

Patches

2
8c7c42674635

Fix snap options in electron builder config

https://github.com/mockoon/mockoonGuillaumeMar 12, 2025via osv
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'
       }
     });
     
    
c7f6e23e87dc

Fix path traversal vulnerability in file paths

https://github.com/mockoon/mockoonGuillaumeMar 5, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.