Skip to content

Security: harden MPP server HMAC/realm/comparison and WWW-Authenticate size cap #545

@EfeDurmaz16

Description

@EfeDurmaz16

Hi — sharing four findings from a cross-language MPP/charge security audit. The Solana @solana/mpp SDK delegates its server-side HMAC issuance/verification, the challenge↔credential binding, expiry, realm handling, and the WWW-Authenticate codec to mppx, so these live here rather than in the consumer. Analyzed against mppx@0.5.5 (dist/ is the runtime source of truth). Each "required fix" is the behavior the audited reference implementation adopted.

Severities are Low–Medium; reporting publicly since the analysis is already published in the pay-kit audit notes. Happy to open a PR if a direction is preferred.

# Severity Title
1 Medium Partial expected-vs-request comparison — most payment fields never pinned
2 Medium Weak HMAC secret key accepted (non-empty check only)
3 Low Default realm is a shared constant across servers
4 Low WWW-Authenticate parser missing size cap

1. Partial expected-vs-request comparison (Medium)

  • Where: dist/server/Mppx.js requestBindingFields / getRequestBindingMismatch, invoked from the verify path.
  • Behavior: the credential↔route binding compares only ['amount','currency','recipient','chainId','memo','splits']. Fields like network, decimals, tokenProgram, feePayer, feePayerKey, externalId, description are never compared, so a credential carrying different values for those than the route configured flows into settlement unchecked (cross-route confusion when two routes share a secret).
  • Fix: exhaustive up-front comparison between the route-built request and the credential's decoded request across all payment-constraining fields (top-level amount,currency,recipient,externalId,description and methodDetails.{network,decimals,tokenProgram,feePayer,feePayerKey,splits}, splits element-wise/order-sensitive). Exclude recentBlockhash (per-challenge state).

2. Weak HMAC secret key accepted (Medium)

  • Where: Mppx.create (if (!secretKey) throw — non-empty only), consumed at dist/Challenge.js (Bytes.fromString(options.secretKey)) with no length check.
  • Behavior: any non-empty string ("key", "a") is accepted as the HMAC-SHA256 key that binds challenge IDs; a low-entropy key enables challenge forgery.
  • Fix: enforce a 32-byte minimum (NIST SP 800-107 for HMAC-SHA256) in Mppx.create, validating both the explicit secretKey and the MPP_SECRET_KEY env path. Document openssl rand -base64 32.

3. Default realm shared across servers (Low)

  • Where: dist/server/Mppx.js const defaultRealm = 'MPP Payment', fallback in resolveRealmFromRequest; realm participates in the cross-route binding and the HMAC ID.
  • Behavior: two services sharing one MPP_SECRET_KEY and both keeping the default realm share a credential namespace — a credential paid against service A passes verification on service B. The Host-header default partially mitigates, but the explicit fallback is a fixed shared string.
  • Fix: derive the default realm from a per-app identity so two services with the same secret get distinct realms; reject explicit realm: ''; keep explicit non-empty realms verbatim.

4. WWW-Authenticate parser missing size cap (Low)

  • Where: dist/Challenge.js deserialize/deserializeList base64-decode + JSON-parse the embedded request parameter with no length guard.
  • Behavior: an oversized header drives proportionally larger decode/parse work than the credential/receipt parsers allow — a client-side DoS surface.
  • Fix: cap the request parameter (e.g. 16 KiB, matching the credential/receipt parsers) before base64-decode/JSON-parse.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions