CVE-2025-24150
Overview
Background
- Copy as cURL
- A Web Inspector Network-tab feature that serializes a recorded HTTP request into an equivalent curl command line for the user to paste into a terminal.
- escapeStringPosix
- A WebInspectorUI helper that wraps a string in POSIX single quotes and escapes embedded quotes so shell metacharacters in the value are not interpreted by the shell.
- curl --data-binary vs --data-raw
- Both send a request body, but
--data/--data-binarytreat a leading ‘@’ as a filename to read, whereas--data-rawsends the argument literally with no ‘@’ interpretation. - Argument injection
- An attack where attacker-controlled data becomes part of a command’s arguments and is interpreted by the invoked program in a way the caller did not intend, even without classic shell metacharacter injection.
Root Cause Analysis
Web Inspector’s Network tab offers a “Copy as cURL” action, implemented in WI.Resource’s command-generation code (Source/WebInspectorUI/UserInterface/Models/Resource.js). When the recorded request carries a body, the generated command appends the request body as a curl data option. For non-form-encoded bodies the old code emitted --data-binary <escaped-body>, where escapeStringPosix() wraps the body in POSIX single quotes to neutralize shell metacharacters. The violated invariant is that the copied command string, when pasted into a shell, must transmit exactly the bytes the page sent and must not cause curl to perform any additional filesystem or side-effecting action. The flaw is that curl’s --data / --data-binary options treat a leading @ in their argument as a request to read the argument’s remainder as a filename (or - as stdin), and shell single-quoting does NOT stop this: the quotes are stripped by the shell, and curl itself then interprets the resulting literal @... string as a file path. So a request body beginning with @ — fully attacker-controlled by the page under inspection (e.g. via fetch/XHR POST) — would make the pasted command read an arbitrary local file (like @/etc/passwd) and upload it, or otherwise act on a filename the victim never intended.
The fix replaces --data-binary with --data-raw; per curl semantics --data-raw is identical to --data except it explicitly does NOT interpret a leading @, so the body is always sent verbatim. The LayoutTest is updated in lockstep to expect --data-raw '{"update":"now"}'. This is not memory corruption; it is an argument-injection / command-construction logic bug where the escaping layer (shell quoting) did not cover the actual sink (curl’s own @-filename parsing).
--data-binary/--data options interpret a leading ‘@’ as a filename after the shell strips the quotes — switching to --data-raw moves the body to a sink that never performs that interpretation.Attack Path
- Serve a page with a crafted request body The attacker controls or influences a web page and causes it to issue an HTTP request whose body begins with ‘@’, e.g. a POST with body ‘@/etc/passwd’ or ‘@~/.ssh/id_rsa’, via fetch() or XMLHttpRequest.
- Victim opens Web Inspector on the request A developer or user inspecting the site opens the Network tab, which records the request and its body.
- Victim uses Copy as cURL
The victim right-clicks the request and selects Copy as cURL; the pre-patch code builds
--data-binary '@/etc/passwd'. - Victim pastes into a shell
The shell strips the single quotes, leaving curl with the literal argument
@/etc/passwd. - curl reads and exfiltrates a local file curl interprets the leading ‘@’ as a file reference, reads the named local file, and sends its contents to the request’s URL (attacker-controlled), disclosing arbitrary local files; other ‘@’/’-’ tricks can redirect input in unintended ways.
Impact Assessment
Files Changed
Source/WebInspectorUI/UserInterface/Models/Resource.jsLayoutTests/http/tests/inspector/network/copy-as-curl.html
Audit Directions
- Other curl option builders in Resource.jsAudit the same Copy-as-cURL generator for any other option whose argument curl parses specially even when quoted; grep the file for ‘command.push(’ and every ‘–’ curl flag (e.g.
--data,-d,--form/-F,--header,--url,--config) and check whether an attacker-controlled leading ‘@’, ‘-’, or ‘=’ could trigger file reads or config loading. - escapeStringPosix reliance across WebInspectorUISearch the WebInspectorUI tree for callers of escapeStringPosix and any command/serialization export (Copy as fetch, Copy as PowerShell, HAR export) that assume shell quoting alone sanitizes values; verify each downstream consumer does not itself interpret sigils like ‘@’.
- Cross-tool 'copy as command' featuresLook for analogous ‘copy as <tool>’ serializers elsewhere (grep for ‘–data-binary’, ‘as-curl’, ‘copyAs’, ’toCurl’) in WebKit and confirm none reintroduce the ‘@’-filename or ‘-’-stdin interpretation for attacker-controlled bodies or headers.
Patch
diff --git a/Source/WebInspectorUI/UserInterface/Models/Resource.js b/Source/WebInspectorUI/UserInterface/Models/Resource.js
index 245045fddcc7..e55d4932412b 100644
--- a/Source/WebInspectorUI/UserInterface/Models/Resource.js
+++ b/Source/WebInspectorUI/UserInterface/Models/Resource.js
@@ -1239,7 +1239,7 @@ WI.Resource = class Resource extends WI.SourceCode
if (this.requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i))
command.push("--data " + escapeStringPosix(this.requestData));
else
- command.push("--data-binary " + escapeStringPosix(this.requestData));
+ command.push("--data-raw " + escapeStringPosix(this.requestData));
}
return command.join(" \\\n");
diff --git a/LayoutTests/http/tests/inspector/network/copy-as-curl.html b/LayoutTests/http/tests/inspector/network/copy-as-curl.html
index 679703b57c43..af0ef882f537 100644
--- a/LayoutTests/http/tests/inspector/network/copy-as-curl.html
+++ b/LayoutTests/http/tests/inspector/network/copy-as-curl.html
@@ -184,7 +184,7 @@ function test()
InspectorTest.expectThat(curl[1] === "-X 'PUT'", "Command should be a PUT request.");
InspectorTest.expectThat(curl.find((cmd) => cmd.includes('Content-Type')) === "-H 'Content-Type: application/json'", "Command should have JSON Content-Type.");
- InspectorTest.expectThat(curl.find((cmd) => cmd === "--data-binary '{\"update\":\"now\"}'") !== undefined, "Command should contain correct JSON data.");
+ InspectorTest.expectThat(curl.find((cmd) => cmd === "--data-raw '{\"update\":\"now\"}'") !== undefined, "Command should contain correct JSON data.");
})
.then(resolve, reject);
Regression Test / PoC
diff --git a/LayoutTests/http/tests/inspector/network/copy-as-curl.html b/LayoutTests/http/tests/inspector/network/copy-as-curl.html
index 679703b57c43..af0ef882f537 100644
--- a/LayoutTests/http/tests/inspector/network/copy-as-curl.html
+++ b/LayoutTests/http/tests/inspector/network/copy-as-curl.html
@@ -184,7 +184,7 @@ function test()
InspectorTest.expectThat(curl[1] === "-X 'PUT'", "Command should be a PUT request.");
InspectorTest.expectThat(curl.find((cmd) => cmd.includes('Content-Type')) === "-H 'Content-Type: application/json'", "Command should have JSON Content-Type.");
- InspectorTest.expectThat(curl.find((cmd) => cmd === "--data-binary '{\"update\":\"now\"}'") !== undefined, "Command should contain correct JSON data.");
+ InspectorTest.expectThat(curl.find((cmd) => cmd === "--data-raw '{\"update\":\"now\"}'") !== undefined, "Command should contain correct JSON data.");
})
.then(resolve, reject);