FlawOpen

flawopen.com/AI Security/hermes-agent-native-signin-redirect-parser-confusion

● FlawOpen mapping: CWE-601 · CVSS v4 8.6[1] · High
FlawOpen Security Research

Hermes Agent native sign-in redirect parsing flaw

Tenable disclosed a high-severity redirect validation flaw in Hermes Agent native sign-in. Versions before 0.21.6 are affected. The v0.21.6 fix reached Docker/Hermes Cloud and CLI/git installs; v0.21.7, released October 11, also lists the Desktop app, Microsoft Store/MSIX, installer, Docker, and Hermes Cloud. The reviewed release notes do not name Termux, so verify that channel separately. Tenable rates it High (CVSS 4.0 8.6); no CVE is assigned. CWE-601 is an editorial mapping for the redirect outcome.[1], [2], [5]

💡 Plain English Explainer (ELI5)

Imagine a receptionist checks an address using one map, writes down the unchecked address exactly as supplied, and later gives it to a courier using a different map. The courier may go somewhere the receptionist never approved. Hermes checked a callback as loopback with Python, then handed its raw form to a browser that can parse it differently.[1]

Core Concepts & Subsystem Terms

Native-app callback
A temporary local web address used by a desktop app to receive the result of a browser-based sign-in.[1], [4]
URL parser differential
Two URL parsers assign different meaning to the same input; validating one interpretation and later using another breaks the security boundary.[1], [3]
PKCE
Proof Key for Code Exchange binds an authorization code to a secret verifier held by the app that began sign-in.[1], [4]
Loopback address
An IP address that routes back to the same device, such as 127.0.0.1 or ::1. The Hermes fix accepts canonical IP literals, not arbitrary remote hosts.[3], [4]

Framework mappings

  • FlawOpen’s conceptual mapping to MITRE CWE; no vendor or CVE record assigned this weakness ID to this research demonstration. CWE-601

Step-by-Step Attack Flow

Step 1

A public native-login route receives a callback

The native sign-in authorization endpoint accepts a redirect URI and PKCE challenge before the user is authenticated.[1]

Step 2

Server and browser interpret the callback differently

The vulnerable validator inspected Python’s parsed hostname but retained the original string. Certain ambiguous URL syntax can make a browser navigate to a different authority.[1], [3]

Step 3

A completed sign-in can expose the authorization code

After the victim completes the normal login, the browser may send the code to the authority it parsed. An attacker who selected the matching PKCE challenge can then attempt to redeem it. The issue requires victim sign-in; it is not a no-interaction account takeover.[1]

Step 4

Canonicalize and update the delivered build

The fix rejects ambiguous syntax and noncanonical authorities, returns the canonical URI, and stores that returned value. Upgrade to v0.21.7 or later through a supported channel; verify Termux separately because the reviewed v0.21.7 notes do not name it.[2], [5], [3]

Source Code: Flaw vs. Secure Implementation

This is an illustrative boundary model, not the complete Hermes Agent source or an exploit. The vulnerable example captures the advisory’s validate-parsed-host/use-raw pattern. The hardened example demonstrates the property in the merged fix: reject ambiguous input, allow only canonical loopback authorities, return the canonical URI, and persist/use that return value. See the linked PR for the actual implementation and tests.[1], [3]

UNPATCHED FLAW
from urllib.parse import urlparse

def validate_callback(raw):
    parsed = urlparse(raw)
    # The original flow checked the parsed host, then retained raw.
    if parsed.scheme == "http" and parsed.hostname in {"127.0.0.1", "::1"}:
        return raw
    raise ValueError("callback must be loopback")
HARDENED SECURE PATCH
from urllib.parse import urlparse, urlunparse

LOOPBACK_HOSTS = {"127.0.0.1": "127.0.0.1", "::1": "[::1]"}

def validate_callback(raw):
    if not raw.isascii() or "\\" in raw or any(ord(c) <= 0x20 or ord(c) == 0x7f for c in raw):
        raise ValueError("callback must use canonical loopback syntax")
    try:
        parsed = urlparse(raw)
        host, port = parsed.hostname, parsed.port
    except ValueError as exc:
        raise ValueError("invalid callback") from exc
    if parsed.scheme != "http" or "#" in raw or parsed.username is not None or parsed.password is not None:
        raise ValueError("callback must be a plain loopback URL")
    authority_host = LOOPBACK_HOSTS.get(host)
    if authority_host is None:
        raise ValueError("callback host must be a loopback IP literal")
    authority = authority_host + (f":{port}" if port is not None else "")
    if parsed.netloc != authority:
        raise ValueError("callback authority is not canonical")
    return urlunparse(parsed._replace(path=parsed.path or "/"))

Engineering & System Hardening Checklist

Sources

← AI Security Hub Directory →