Skip to content

sign_request strips query string but not URL fragment, producing 401 on '#'-bearing paths #317

Closed

Description

@TexasCoding

Problem

KalshiAuth.sign_request strips the query string before signing but leaves any #fragment intact. Per RFC 3986 §3.5, fragments are client-side; httpx (like every conforming HTTP client) drops them from the request-target before transmission. The signer therefore signs /trade-api/v2/markets/EVT#tab=foo while httpx sends /trade-api/v2/markets/EVT — a guaranteed signature mismatch, surfacing as a 401 with KalshiAuthError and no hint that a stray # is the cause.

Realistic triggers: a caller templating a path with a UI deep-link, an #anchor inherited from a copy-paste, or any custom path-builder that doesn't sanitize fragments.

Evidence

  • kalshi/auth.py:262-273:
if timestamp_ms is None:
    timestamp_ms = int(time.time() * 1000)

# Strip query parameters before signing
clean_path = path.split("?")[0]

# Strip trailing slash for canonical form
if clean_path != "/" and clean_path.endswith("/"):
    clean_path = clean_path.rstrip("/")

# Normalize percent-encoding to uppercase hex (RFC 3986 section 2.1)
clean_path = _normalize_percent_encoding(clean_path)

Only ? is stripped; # survives into the signed message.

  • kalshi/_base_client.py (_map_error) routes 401 to KalshiAuthError with no indication that the fragment caused it, and 401 is correctly absent from RETRYABLE_STATUS_CODES.

Suggested fix

Apply the same shape as the ? strip:

clean_path = path.split("?")[0]
clean_path = clean_path.split("#")[0]

Mirror the strip in any wire-path canonicalization upstream so signing and httpx see byte-identical bytes. Add a regression test asserting sign_request("GET", "/trade-api/v2/markets/EVT#tab=foo") produces the same signature as sign_request("GET", "/trade-api/v2/markets/EVT").

Source

Round-3 independent audit (reviewer: security_auth).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsecuritySecurity-related concern

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions