CVE-2025-24150
Overview
Impact:
Description: Copying a URL from Web Inspector may lead to command injection
Researchers: Johan Carlsson (joaxcar)
Key insight. The escaping was applied at the wrong layer: shell single-quoting protects against the shell, but the dangerous parsing happens inside curl, whose
--data-binary/--dataoptions interpret a leading ‘@’ as a filename after the shell strips the quotes — switching to--data-rawmoves the body to a sink that never performs that interpretation.
Patch Details
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);
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");
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2025-24150 |
| Bugzilla | 283718 |
| Component | WebKit |
| Bug Class | LogicError |
| Severity | medium |
| Commit | fed47c6e0559b588… |
| Advisory | Apple Advisory |
Terminology
- 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).
Attack Path
1. 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.
2. 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.
3. 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'.
4. Victim pastes into a shell
The shell strips the single quotes, leaving curl with the literal argument @/etc/passwd.
5. 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
The primitive is local file disclosure / unintended file access by curl, gated on significant user interaction: a victim must open Web Inspector on an attacker-influenced request, choose Copy as cURL, and paste the result into a shell. There is no memory corruption and nothing runs in the WebContent/GPU/Network sandbox as a result of the browser code itself; the side effect executes in the victim’s own shell/user context with their filesystem privileges. Severity is medium precisely because it is a data-exfiltration/argument-injection issue rather than code execution in the engine, and it requires the multi-step manual paste. Escalation is limited to reading files curl can access and sending them to the request URL; it is not a stepping stone to browser-process RCE.
Audit Directions
Other curl option builders in Resource.js
Audit 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 WebInspectorUI
Search 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’ features
Look 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.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
WI.Resource.prototype (Copy as cURL command builder) |
Source/WebInspectorUI/UserInterface/Models/Resource.js |
modified | Changed the non-form-encoded body branch to push --data-raw <escaped> instead of --data-binary <escaped>, so curl no longer treats a leading ‘@’ in the body as a filename. |
test (copy-as-curl layout test) |
LayoutTests/http/tests/inspector/network/copy-as-curl.html |
modified | Updated the expectation from --data-binary '{...}' to --data-raw '{...}' to match the corrected output. |
Regression Test
The fix ships this proof-of-concept / regression test:
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);