VYPR
Moderate severityNVD Advisory· Published Sep 17, 2025· Updated Jan 26, 2026

CVE-2025-56648

CVE-2025-56648

Description

npm parcel 2.0.0-alpha and before has an Origin Validation Error vulnerability. Malicious websites can send XMLHTTPRequests to the application's development server and read the response to steal source code when developers visit them.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

In Parcel before 2.0.0, the dev server did not validate Origin headers, allowing malicious websites to steal source code via cross-origin requests.

CVE-2025-56648 is an origin validation error in Parcel's development server, affecting versions 2.0.0-alpha and earlier. The server responded with Access-Control-Allow-Origin: * on all requests, meaning any web page could make cross-origin reads of the served files [1][2].

An attacker only needs to lure a developer who is running a Parcel dev server into visiting a malicious website. That website can then issue XMLHttpRequests to localhost (or the dev server's address) and read the response due to the permissive CORS policy, thereby stealing the application's source code [2].

The impact is unauthorised access to the entire source tree of the project being developed, which could include sensitive logic, secrets embedded in code, or proprietary algorithms [2].

The vulnerability has been patched in later commits that add a --no-cors flag and implement proper Origin and Host header verification; specifically, the dev server now checks the Origin against an allowed set of hosts (localhost and local IPs) before granting CORS access [1][3]. Users should upgrade to a version containing the fix or use the --no-cors option as a workaround.

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@parcel/reporter-dev-servernpm
>= 1.6.1, < 2.16.42.16.4

Affected products

1
  • npm/parceldescription

Patches

2
9e2f6f137712

Add --no-cors option (#10268)

https://github.com/parcel-bundler/parcelDevon GovettFeb 2, 2026via ghsa
7 files changed · +78 13
  • packages/core/integration-tests/test/server.js+53 0 modified
    @@ -636,4 +636,57 @@ describe('server', function () {
     
         assert(data.includes(path.basename(localCSS.filePath)));
       });
    +
    +  it('should support cors by default', async function () {
    +    let port = await getPort();
    +    let b = bundler(path.join(__dirname, '/integration/commonjs/index.js'), {
    +      defaultTargetOptions: {
    +        distDir,
    +      },
    +      config,
    +      serveOptions: {
    +        https: false,
    +        port: port,
    +        host: 'localhost',
    +      },
    +    });
    +
    +    subscription = await b.watch();
    +    await getNextBuild(b);
    +
    +    let res = await fetch(`http://localhost:${port}/index.js`);
    +    assert.equal(res.headers.get('Access-Control-Allow-Origin'), '*');
    +    assert.equal(
    +      res.headers.get('Access-Control-Allow-Methods'),
    +      'GET, HEAD, PUT, PATCH, POST, DELETE',
    +    );
    +    assert.equal(
    +      res.headers.get('Access-Control-Allow-Headers'),
    +      'Origin, X-Requested-With, Content-Type, Accept, Content-Type',
    +    );
    +  });
    +
    +  it('should support --no-cors', async function () {
    +    let port = await getPort();
    +    let b = bundler(path.join(__dirname, '/integration/commonjs/index.js'), {
    +      defaultTargetOptions: {
    +        distDir,
    +      },
    +      config,
    +      serveOptions: {
    +        https: false,
    +        port: port,
    +        host: 'localhost',
    +        cors: false,
    +      },
    +    });
    +
    +    subscription = await b.watch();
    +    await getNextBuild(b);
    +
    +    let res = await fetch(`http://localhost:${port}/index.js`);
    +    assert.equal(res.headers.has('Access-Control-Allow-Origin'), false);
    +    assert.equal(res.headers.has('Access-Control-Allow-Methods'), false);
    +    assert.equal(res.headers.has('Access-Control-Allow-Headers'), false);
    +  });
     });
    
  • packages/core/parcel/src/cli.js+4 1 modified
    @@ -173,6 +173,7 @@ var hmrOptions = {
       '--key <path>': 'path to private key to use with HTTPS',
       '--hmr-port <port>': ['hot module replacement port', process.env.HMR_PORT],
       '--hmr-host <host>': ['hot module replacement host', process.env.HMR_HOST],
    +  '--no-cors': 'disable cors',
     };
     
     function applyOptions(cmd, options) {
    @@ -484,13 +485,14 @@ async function normalizeOptions(
       }
     
       if (command.name() === 'serve') {
    -    let {publicUrl} = command;
    +    let {publicUrl, cors} = command;
     
         serveOptions = {
           https,
           port,
           host,
           publicUrl,
    +      cors,
         };
       }
     
    @@ -502,6 +504,7 @@ async function normalizeOptions(
         hmrOptions = {
           port: hmrport,
           host: hmrhost,
    +      cors: command.cors,
         };
       }
     
    
  • packages/core/types-internal/src/index.js+3 0 modified
    @@ -103,6 +103,7 @@ export type RawParcelConfigPipeline = Array<PackageName>;
     export type HMROptions = {
       port?: number,
       host?: string,
    +  cors?: boolean,
       ...
     };
     
    @@ -405,6 +406,7 @@ export type InitialServerOptions = {|
       +host?: string,
       +port: number,
       +https?: HTTPSOptions | boolean,
    +  +cors?: boolean,
     |};
     
     export interface PluginOptions {
    @@ -432,6 +434,7 @@ export type ServerOptions = {|
       +port: number,
       +https?: HTTPSOptions | boolean,
       +publicUrl?: string,
    +  +cors?: boolean,
     |};
     
     export type HTTPSOptions = {|
    
  • packages/reporters/dev-server/src/HMRServer.js+1 1 modified
    @@ -110,7 +110,7 @@ export default class HMRServer {
             outputFS: this.options.outputFS,
             cacheDir: this.options.cacheDir,
             listener: (req, res) => {
    -          setHeaders(res);
    +          setHeaders(res, this.options.cors ?? true);
               if (req.method === 'OPTIONS') {
                 res.statusCode = 200;
                 res.end();
    
  • packages/reporters/dev-server/src/Server.js+13 11 modified
    @@ -32,16 +32,18 @@ import {createProxyMiddleware} from 'http-proxy-middleware';
     import {URL} from 'url';
     import fresh from 'fresh';
     
    -export function setHeaders(res: Response) {
    -  res.setHeader('Access-Control-Allow-Origin', '*');
    -  res.setHeader(
    -    'Access-Control-Allow-Methods',
    -    'GET, HEAD, PUT, PATCH, POST, DELETE',
    -  );
    -  res.setHeader(
    -    'Access-Control-Allow-Headers',
    -    'Origin, X-Requested-With, Content-Type, Accept, Content-Type',
    -  );
    +export function setHeaders(res: Response, cors: boolean) {
    +  if (cors) {
    +    res.setHeader('Access-Control-Allow-Origin', '*');
    +    res.setHeader(
    +      'Access-Control-Allow-Methods',
    +      'GET, HEAD, PUT, PATCH, POST, DELETE',
    +    );
    +    res.setHeader(
    +      'Access-Control-Allow-Headers',
    +      'Origin, X-Requested-With, Content-Type, Accept, Content-Type',
    +    );
    +  }
       res.setHeader('Cache-Control', 'max-age=0, must-revalidate');
     }
     
    @@ -479,7 +481,7 @@ export default class Server {
     
         const app = connect();
         app.use((req, res, next) => {
    -      setHeaders(res);
    +      setHeaders(res, this.options.cors ?? true);
           if (req.method === 'OPTIONS') {
             res.statusCode = 200;
             res.end();
    
  • packages/reporters/dev-server/src/ServerReporter.js+3 0 modified
    @@ -150,6 +150,7 @@ async function startDevServer(options, logger, isBrowser) {
             projectRoot: options.projectRoot,
             distDir: serveOptions.distDir,
             publicUrl: serveOptions.publicUrl ?? '/',
    +        cors: serveOptions.cors,
           };
           hmrServer = new HMRServer(hmrServerOptions);
           hmrServers.set(serveOptions.port, hmrServer);
    @@ -171,6 +172,8 @@ async function startDevServer(options, logger, isBrowser) {
           projectRoot: options.projectRoot,
           distDir: serveOptions ? serveOptions.distDir : null,
           publicUrl: serveOptions ? serveOptions.publicUrl ?? '/' : '/',
    +      cors:
    +        hmrOptions?.cors ?? (serveOptions ? serveOptions.cors : true) ?? true,
         };
         hmrServer = new HMRServer(hmrServerOptions);
         hmrServers.set(port, hmrServer);
    
  • packages/reporters/dev-server/src/types.js.flow+1 0 modified
    @@ -56,4 +56,5 @@ export type HMRServerOptions = {|
       projectRoot: FilePath,
       distDir: ?FilePath,
       publicUrl: string,
    +  cors: ?boolean
     |};
    
4bc56e3242a8

Verify Host and Origin headers in dev server

https://github.com/parcel-bundler/parcelDevon GovettApr 20, 2025via ghsa
3 files changed · +107 10
  • flow-libs/ws.js.flow+7 3 modified
    @@ -14,6 +14,10 @@ declare type ws$PerMessageDeflateOptions = {|
       maxPayload?: number,
     |};
     
    +declare type ws$ClientInfo = {|
    +  origin: string
    +|};
    +
     // $FlowFixMe[incompatible-extend]
     declare class ws$WebSocketServer extends events$EventEmitter {
       /**
    @@ -31,7 +35,7 @@ declare class ws$WebSocketServer extends events$EventEmitter {
           perMessageDeflate?: boolean | ws$PerMessageDeflateOptions,
           port: number,
           server?: http$Server | https$Server,
    -      verifyClient?: () => mixed,
    +      verifyClient?: (info: ws$ClientInfo) => mixed,
         |},
         callback?: () => mixed
       ): this;
    @@ -47,7 +51,7 @@ declare class ws$WebSocketServer extends events$EventEmitter {
           perMessageDeflate?: boolean | ws$PerMessageDeflateOptions,
           port?: number,
           server: http$Server | https$Server,
    -      verifyClient?: () => mixed,
    +      verifyClient?: (info: ws$ClientInfo) => mixed,
         |},
         callback?: () => mixed
       ): this;
    @@ -63,7 +67,7 @@ declare class ws$WebSocketServer extends events$EventEmitter {
           perMessageDeflate?: boolean | ws$PerMessageDeflateOptions,
           port?: number,
           server?: http$Server | https$Server,
    -      verifyClient?: () => mixed,
    +      verifyClient?: (info: ws$ClientInfo) => mixed,
         |},
         callback?: () => mixed
       ): this;
    
  • packages/reporters/dev-server/src/HMRServer.js+13 3 modified
    @@ -14,7 +14,7 @@ import type {
       Request,
       Response,
     } from './types.js.flow';
    -import {setHeaders, SOURCES_ENDPOINT} from './Server';
    +import {setHeaders, verifyOrigin, SOURCES_ENDPOINT} from './Server';
     
     import nullthrows from 'nullthrows';
     import url, {fileURLToPath} from 'url';
    @@ -110,7 +110,7 @@ export default class HMRServer {
             outputFS: this.options.outputFS,
             cacheDir: this.options.cacheDir,
             listener: (req, res) => {
    -          setHeaders(res);
    +          setHeaders(req.headers.origin, this.options.host, res);
               if (req.method === 'OPTIONS') {
                 res.statusCode = 200;
                 res.end();
    @@ -128,7 +128,17 @@ export default class HMRServer {
         } else {
           this.options.addMiddleware?.((req, res) => this.handle(req, res));
         }
    -    this.wss = new WebSocket.Server({server});
    +    this.wss = new WebSocket.Server({
    +      server,
    +      verifyClient: info => {
    +        // Validate Origin header to prevent Cross-Site WebSocket Hijacking.
    +        // If there is no Origin header, assume this request is from Node.js or another non-browser client.
    +        if (!info.origin) {
    +          return true;
    +        }
    +        return verifyOrigin(info.origin, this.options.host);
    +      },
    +    });
     
         this.wss.on('connection', ws => {
           if (this.unresolvedError) {
    
  • packages/reporters/dev-server/src/Server.js+87 4 modified
    @@ -31,9 +31,39 @@ import serveHandler from 'serve-handler';
     import {createProxyMiddleware} from 'http-proxy-middleware';
     import {URL} from 'url';
     import fresh from 'fresh';
    +import os from 'os';
    +
    +// Default allowed hosts include `localhost` and all local ip addresses.
    +let defaultAllowedHostsCache = null;
    +export function getDefaultAllowedHosts(): string[] {
    +  if (!defaultAllowedHostsCache) {
    +    defaultAllowedHostsCache = ['localhost'];
    +    let interfaces = os.networkInterfaces();
    +    for (let name in interfaces) {
    +      for (let addr of interfaces[name]) {
    +        defaultAllowedHostsCache.push(
    +          addr.family === 'IPv6' ? `[${addr.address}]` : addr.address,
    +        );
    +      }
    +    }
    +  }
    +
    +  return defaultAllowedHostsCache;
    +}
    +
    +export function setHeaders(
    +  origin: ?string,
    +  allowedHostname: ?string,
    +  res: Response,
    +) {
    +  res.setHeader('Cache-Control', 'max-age=0, must-revalidate');
    +
    +  // Add CORS headers if the Origin header is valid.
    +  if (!origin || !verifyOrigin(origin, allowedHostname)) {
    +    return;
    +  }
     
    -export function setHeaders(res: Response) {
    -  res.setHeader('Access-Control-Allow-Origin', '*');
    +  res.setHeader('Access-Control-Allow-Origin', origin);
       res.setHeader(
         'Access-Control-Allow-Methods',
         'GET, HEAD, PUT, PATCH, POST, DELETE',
    @@ -42,7 +72,53 @@ export function setHeaders(res: Response) {
         'Access-Control-Allow-Headers',
         'Origin, X-Requested-With, Content-Type, Accept, Content-Type',
       );
    -  res.setHeader('Cache-Control', 'max-age=0, must-revalidate');
    +}
    +
    +export function verifyOrigin(
    +  origin: string,
    +  allowedHostname: ?string,
    +): boolean {
    +  try {
    +    let url = new URL(origin);
    +    if (
    +      url.protocol === 'file:' ||
    +      url.protocol === 'chrome-extension:' ||
    +      url.protocol === 'moz-extension:'
    +    ) {
    +      return true;
    +    }
    +
    +    return verifyHost(url.hostname, allowedHostname);
    +  } catch {
    +    return false;
    +  }
    +}
    +
    +export function verifyHost(
    +  requestHost: string,
    +  allowedHostname: ?string,
    +): boolean {
    +  // IPv6 address
    +  if (requestHost[0] === '[') {
    +    let index = requestHost.indexOf(']');
    +    if (index < 0) {
    +      return false;
    +    }
    +
    +    requestHost = requestHost.slice(0, index + 1);
    +  } else {
    +    // Remove port
    +    let index = requestHost.indexOf(':');
    +    if (index >= 0) {
    +      requestHost = requestHost.slice(0, index);
    +    }
    +  }
    +
    +  if (allowedHostname) {
    +    return requestHost === allowedHostname;
    +  }
    +
    +  return getDefaultAllowedHosts().includes(requestHost);
     }
     
     const SLASH_REGEX = /\//g;
    @@ -479,7 +555,14 @@ export default class Server {
     
         const app = connect();
         app.use((req, res, next) => {
    -      setHeaders(res);
    +      // Validate the Host header to prevent DNS rebinding attacks.
    +      if (!verifyHost(req.headers.host, this.options.host)) {
    +        res.statusCode = 403;
    +        res.end('Host not allowed.');
    +        return;
    +      }
    +
    +      setHeaders(req.headers.origin, this.options.host, res);
           if (req.method === 'OPTIONS') {
             res.statusCode = 200;
             res.end();
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.