Skip to content

Require Cryptographic Authorization for Server-Side Batch Signing #696

Description

@jahrulezfrancis

Problem

When server-side signing is enabled, the only "authorization" check on
/api/batch-submit is comparing the caller-supplied publicKey against the
public key derived from the server's own STELLAR_SECRET_KEY. A Stellar
public key is not secret information — it's visible on-chain after the
account's first transaction — so this check doesn't prove the caller
actually controls the signing key. It looks like an authorization check but
isn't one.

Severity: High
Location: app/api/batch-submit/route.ts:279-318

Description

DEPLOYMENT.md correctly documents ALLOW_SERVER_SIGNING as an
internal/trusted-deployment-only feature, defaulting to false, and warns
operators to gate it with network-level controls (VPN, IP allowlist, mTLS)
rather than relying on this route alone. That network-layer guidance is
sound and already reduces real-world exposure. The issue is one layer down:
the code's own signingPublicKey !== publicKey check (line 311) provides
zero cryptographic proof of caller identity — it only compares two public
values. If the documented network-layer control is ever misconfigured,
briefly exposed in a staging environment, or simply not applied by an
operator who trusts the in-code check to be doing real authentication, there
is no actual defense-in-depth behind it. Anyone who knows or looks up the
configured account's G-address can pass it as publicKey and have the
server sign and submit a payment to an address of their choosing.

Steps to Reproduce

  1. Enable ALLOW_SERVER_SIGNING=true with STELLAR_SECRET_KEY set (e.g. in
    a staging environment).
  2. Obtain that account's public G-address — trivially available from any
    prior on-chain activity of that account.
  3. POST /api/batch-submit with that publicKey and a payments array
    directing funds to an attacker-controlled address.
  4. The server accepts the request (signingPublicKey === publicKey passes)
    and signs/submits the payment using STELLAR_SECRET_KEY.

Expected Behavior

Server-signing requests should require proof of authorization beyond a
public value match — the code itself should not rely solely on the
network-layer mitigation to be safe, since that mitigation lives outside the
codebase and isn't guaranteed to be correctly applied in every deployment.

Suggested Fix Approach

Require a cryptographic proof of caller identity (e.g. a signed challenge,
a validated API key/session bound to the operator, or scoped credentials)
before accepting a server-signing request — not just a public-key string
match. Alternatively, remove the public custodial path entirely and accept
only pre-signed client XDRs, pushing all signing authority to the wallet
holder.

Acceptance Criteria

  • Server-signing requests require a credential that cannot be derived
    from public on-chain data
  • A request with a correct publicKey but no valid credential is
    rejected (401/403)
  • Existing legitimate server-signing test flow
    (tests/batch-submit.test.ts) still passes with the new credential
    requirement added
  • DEPLOYMENT.md updated to reflect that the code now enforces
    authorization directly, in addition to the existing network-layer
    guidance

Labels: Maybe Rewarded, GrantFox OSS, Official Campaign | FWC26, bug, security

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions