VYPR
Critical severity9.1NVD Advisory· Published Oct 15, 2024· Updated Apr 15, 2026

CVE-2024-48914

CVE-2024-48914

Description

Vendure is an open-source headless commerce platform. Prior to versions 3.0.5 and 2.3.3, a vulnerability in Vendure's asset server plugin allows an attacker to craft a request which is able to traverse the server file system and retrieve the contents of arbitrary files, including sensitive data such as configuration files, environment variables, and other critical data stored on the server. In the same code path is an additional vector for crashing the server via a malformed URI. Patches are available in versions 3.0.5 and 2.3.3. Some workarounds are also available. One may use object storage rather than the local file system, e.g. MinIO or S3, or define middleware which detects and blocks requests with urls containing /../.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@vendure/asset-server-pluginnpm
< 2.3.32.3.3
@vendure/asset-server-pluginnpm
>= 3.0.0, < 3.0.53.0.5

Patches

6
e2ee0c43159b

Merge commit from fork

https://github.com/vendure-ecommerce/vendureMichael BromleyOct 15, 2024via ghsa
2 files changed · +54 6
  • packages/asset-server-plugin/e2e/asset-server-plugin.e2e-spec.ts+38 2 modified
    @@ -1,7 +1,8 @@
     /* eslint-disable @typescript-eslint/no-non-null-assertion */
    -import { mergeConfig } from '@vendure/core';
    +import { ConfigService, mergeConfig } from '@vendure/core';
     import { AssetFragment } from '@vendure/core/e2e/graphql/generated-e2e-admin-types';
     import { createTestEnvironment } from '@vendure/testing';
    +import { exec } from 'child_process';
     import fs from 'fs-extra';
     import gql from 'graphql-tag';
     import fetch from 'node-fetch';
    @@ -193,6 +194,41 @@ describe('AssetServerPlugin', () => {
             it('does not error on non-integer height', async () => {
                 return fetch(`${asset.preview}?h=10.5`);
             });
    +
    +        // https://github.com/vendure-ecommerce/vendure/security/advisories/GHSA-r9mq-3c9r-fmjq
    +        describe('path traversal', () => {
    +            function curlWithPathAsIs(url: string) {
    +                return new Promise<string>((resolve, reject) => {
    +                    // We use curl here rather than node-fetch or any other fetch-type function because
    +                    // those will automatically perform path normalization which will mask the path traversal
    +                    return exec(`curl --path-as-is ${url}`, (err, stdout, stderr) => {
    +                        if (err) {
    +                            reject(err);
    +                        }
    +                        resolve(stdout);
    +                    });
    +                });
    +            }
    +
    +            function testPathTraversalOnUrl(urlPath: string) {
    +                return async () => {
    +                    const port = server.app.get(ConfigService).apiOptions.port;
    +                    const result = await curlWithPathAsIs(`http://localhost:${port}/assets${urlPath}`);
    +                    expect(result).not.toContain('@vendure/asset-server-plugin');
    +                    expect(result.toLowerCase()).toContain('resource not found');
    +                };
    +            }
    +
    +            it('blocks path traversal 1', testPathTraversalOnUrl(`/../../package.json`));
    +            it('blocks path traversal 2', testPathTraversalOnUrl(`/foo/../../../package.json`));
    +            it('blocks path traversal 3', testPathTraversalOnUrl(`/foo/../../../foo/../package.json`));
    +            it('blocks path traversal 4', testPathTraversalOnUrl(`/%2F..%2F..%2Fpackage.json`));
    +            it('blocks path traversal 5', testPathTraversalOnUrl(`/%2E%2E/%2E%2E/package.json`));
    +            it('blocks path traversal 6', testPathTraversalOnUrl(`/..//..//package.json`));
    +            it('blocks path traversal 7', testPathTraversalOnUrl(`/.%2F.%2F.%2Fpackage.json`));
    +            it('blocks path traversal 8', testPathTraversalOnUrl(`/..\\\\..\\\\package.json`));
    +            it('blocks path traversal 9', testPathTraversalOnUrl(`/\\\\\\..\\\\\\..\\\\\\package.json`));
    +        });
         });
     
         describe('deletion', () => {
    @@ -268,7 +304,7 @@ describe('AssetServerPlugin', () => {
         // https://github.com/vendure-ecommerce/vendure/issues/1563
         it('falls back to binary preview if image file cannot be processed', async () => {
             const filesToUpload = [path.join(__dirname, 'fixtures/assets/bad-image.jpg')];
    -        const { createAssets }: CreateAssets.Mutation = await adminClient.fileUploadMutation({
    +        const { createAssets }: CreateAssetsMutation = await adminClient.fileUploadMutation({
                 mutation: CREATE_ASSETS,
                 filePaths: filesToUpload,
                 mapVariables: filePaths => ({
    
  • packages/asset-server-plugin/src/plugin.ts+16 4 modified
    @@ -281,7 +281,7 @@ export class AssetServerPlugin implements NestModule, OnApplicationBootstrap {
             return async (err: any, req: Request, res: Response, next: NextFunction) => {
                 if (err && (err.status === 404 || err.statusCode === 404)) {
                     if (req.query) {
    -                    const decodedReqPath = decodeURIComponent(req.path);
    +                    const decodedReqPath = this.sanitizeFilePath(req.path);
                         Logger.debug(`Pre-cached Asset not found: ${decodedReqPath}`, loggerCtx);
                         let file: Buffer;
                         try {
    @@ -347,9 +347,7 @@ export class AssetServerPlugin implements NestModule, OnApplicationBootstrap {
                 imageParamsString += quality;
             }
     
    -        /* eslint-enable @typescript-eslint/restrict-template-expressions */
    -
    -        const decodedReqPath = decodeURIComponent(req.path);
    +        const decodedReqPath = this.sanitizeFilePath(req.path);
             if (imageParamsString !== '') {
                 const imageParamHash = this.md5(imageParamsString);
                 return path.join(this.cacheDir, this.addSuffix(decodedReqPath, imageParamHash, imageFormat));
    @@ -358,6 +356,20 @@ export class AssetServerPlugin implements NestModule, OnApplicationBootstrap {
             }
         }
     
    +    /**
    +     * Sanitize the file path to prevent directory traversal attacks.
    +     */
    +    private sanitizeFilePath(filePath: string): string {
    +        let decodedPath: string;
    +        try {
    +            decodedPath = decodeURIComponent(filePath);
    +        } catch (e: any) {
    +            Logger.error((e.message as string) + ': ' + filePath, loggerCtx);
    +            return '';
    +        }
    +        return path.normalize(decodedPath).replace(/(\.\.[\/\\])+/, '');
    +    }
    +
         private md5(input: string): string {
             return createHash('md5').update(input).digest('hex');
         }
    
e4b58af6822d

Merge commit from fork

https://github.com/vendure-ecommerce/vendureMichael BromleyOct 15, 2024via ghsa
2 files changed · +54 6
  • packages/asset-server-plugin/e2e/asset-server-plugin.e2e-spec.ts+38 2 modified
    @@ -1,7 +1,8 @@
     /* eslint-disable @typescript-eslint/no-non-null-assertion */
    -import { mergeConfig } from '@vendure/core';
    +import { ConfigService, mergeConfig } from '@vendure/core';
     import { AssetFragment } from '@vendure/core/e2e/graphql/generated-e2e-admin-types';
     import { createTestEnvironment } from '@vendure/testing';
    +import { exec } from 'child_process';
     import fs from 'fs-extra';
     import gql from 'graphql-tag';
     import fetch from 'node-fetch';
    @@ -193,6 +194,41 @@ describe('AssetServerPlugin', () => {
             it('does not error on non-integer height', async () => {
                 return fetch(`${asset.preview}?h=10.5`);
             });
    +
    +        // https://github.com/vendure-ecommerce/vendure/security/advisories/GHSA-r9mq-3c9r-fmjq
    +        describe('path traversal', () => {
    +            function curlWithPathAsIs(url: string) {
    +                return new Promise<string>((resolve, reject) => {
    +                    // We use curl here rather than node-fetch or any other fetch-type function because
    +                    // those will automatically perform path normalization which will mask the path traversal
    +                    return exec(`curl --path-as-is ${url}`, (err, stdout, stderr) => {
    +                        if (err) {
    +                            reject(err);
    +                        }
    +                        resolve(stdout);
    +                    });
    +                });
    +            }
    +
    +            function testPathTraversalOnUrl(urlPath: string) {
    +                return async () => {
    +                    const port = server.app.get(ConfigService).apiOptions.port;
    +                    const result = await curlWithPathAsIs(`http://localhost:${port}/assets${urlPath}`);
    +                    expect(result).not.toContain('@vendure/asset-server-plugin');
    +                    expect(result.toLowerCase()).toContain('resource not found');
    +                };
    +            }
    +
    +            it('blocks path traversal 1', testPathTraversalOnUrl(`/../../package.json`));
    +            it('blocks path traversal 2', testPathTraversalOnUrl(`/foo/../../../package.json`));
    +            it('blocks path traversal 3', testPathTraversalOnUrl(`/foo/../../../foo/../package.json`));
    +            it('blocks path traversal 4', testPathTraversalOnUrl(`/%2F..%2F..%2Fpackage.json`));
    +            it('blocks path traversal 5', testPathTraversalOnUrl(`/%2E%2E/%2E%2E/package.json`));
    +            it('blocks path traversal 6', testPathTraversalOnUrl(`/..//..//package.json`));
    +            it('blocks path traversal 7', testPathTraversalOnUrl(`/.%2F.%2F.%2Fpackage.json`));
    +            it('blocks path traversal 8', testPathTraversalOnUrl(`/..\\\\..\\\\package.json`));
    +            it('blocks path traversal 9', testPathTraversalOnUrl(`/\\\\\\..\\\\\\..\\\\\\package.json`));
    +        });
         });
     
         describe('deletion', () => {
    @@ -268,7 +304,7 @@ describe('AssetServerPlugin', () => {
         // https://github.com/vendure-ecommerce/vendure/issues/1563
         it('falls back to binary preview if image file cannot be processed', async () => {
             const filesToUpload = [path.join(__dirname, 'fixtures/assets/bad-image.jpg')];
    -        const { createAssets }: CreateAssets.Mutation = await adminClient.fileUploadMutation({
    +        const { createAssets }: CreateAssetsMutation = await adminClient.fileUploadMutation({
                 mutation: CREATE_ASSETS,
                 filePaths: filesToUpload,
                 mapVariables: filePaths => ({
    
  • packages/asset-server-plugin/src/plugin.ts+16 4 modified
    @@ -281,7 +281,7 @@ export class AssetServerPlugin implements NestModule, OnApplicationBootstrap {
             return async (err: any, req: Request, res: Response, next: NextFunction) => {
                 if (err && (err.status === 404 || err.statusCode === 404)) {
                     if (req.query) {
    -                    const decodedReqPath = decodeURIComponent(req.path);
    +                    const decodedReqPath = this.sanitizeFilePath(req.path);
                         Logger.debug(`Pre-cached Asset not found: ${decodedReqPath}`, loggerCtx);
                         let file: Buffer;
                         try {
    @@ -347,9 +347,7 @@ export class AssetServerPlugin implements NestModule, OnApplicationBootstrap {
                 imageParamsString += quality;
             }
     
    -        /* eslint-enable @typescript-eslint/restrict-template-expressions */
    -
    -        const decodedReqPath = decodeURIComponent(req.path);
    +        const decodedReqPath = this.sanitizeFilePath(req.path);
             if (imageParamsString !== '') {
                 const imageParamHash = this.md5(imageParamsString);
                 return path.join(this.cacheDir, this.addSuffix(decodedReqPath, imageParamHash, imageFormat));
    @@ -358,6 +356,20 @@ export class AssetServerPlugin implements NestModule, OnApplicationBootstrap {
             }
         }
     
    +    /**
    +     * Sanitize the file path to prevent directory traversal attacks.
    +     */
    +    private sanitizeFilePath(filePath: string): string {
    +        let decodedPath: string;
    +        try {
    +            decodedPath = decodeURIComponent(filePath);
    +        } catch (e: any) {
    +            Logger.error((e.message as string) + ': ' + filePath, loggerCtx);
    +            return '';
    +        }
    +        return path.normalize(decodedPath).replace(/(\.\.[\/\\])+/, '');
    +    }
    +
         private md5(input: string): string {
             return createHash('md5').update(input).digest('hex');
         }
    

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.