Skip to content

Any user can store arbitrary strings on chain that the frontend will render as HTML #214

Description

@Muyideen-js

Problem

Three facts combine into a stored cross-site scripting vulnerability on an origin that holds live
wallet connections.

1. The chain accepts arbitrary attacker-controlled strings.

referral_registry::register_referral(user, display_name, referrer) is gated only by
user.require_auth() -- any funded account may call it. The display_name it stores is written
straight to persistent storage with no length limit, no character restriction, and no
sanitisation of any kind
:

env.storage()
    .persistent()
    .set(&DataKey::DisplayName(user.clone()), &display_name);

There is no validation anywhere in register_referral. (The PR that would have capped it at 64
characters was never merged -- and 64 characters is more than enough for a payload regardless.)
get_display_name(user) hands the string back to any caller.

2. The frontend's entire rendering pipeline is innerHTML with unescaped template literals.

Every list in the app is built by string concatenation and assigned to innerHTML:

  • frontend/pages.js:2 -- market cards, interpolating m.title, m.category, m.close
  • frontend/pages.js:3 -- leaderboard rows, interpolating p.name and p.address
  • frontend/script.js:126 -- market list, interpolating market.title, market.detail
  • frontend/script.js:186 -- position list, interpolating position.explorerUrl into an href

Nothing is escaped. There is no textContent on any of these paths, no sanitiser, no templating
library that would escape by default.

3. Two open issues are about to connect the two.

The leaderboard issue and the referral issue both call for replacing the hardcoded leaders array
with real on-chain identities, which is exactly get_display_name. The moment that lands, an
attacker-supplied string flows into innerHTML.

Impact

A single register_referral call with a display_name of

<img src=x onerror="...">

stores executable markup on chain. It then runs in the browser of every visitor to the
leaderboard
, permanently, because it lives on chain rather than in a database anyone can edit.

The origin it executes on is not a blog. It is the origin that:

  • holds the connected wallet session
  • builds Soroban transactions
  • calls the wallet's signTransaction

so injected script can rewrite the operation before the user is asked to approve it, prompt for
signatures of its own, read every address the visitor has connected, or replace the market contract
address in memory. There is currently no Content-Security-Policy on the site to contain any of
it -- see the CDN issue for the confirmed missing headers.

The payload is also censorship-resistant in the worst way: it cannot be deleted, because it is in
contract storage. Removing it means a contract migration or a frontend blocklist.

What to do

  • Stop using innerHTML for anything carrying dynamic data. Build nodes and assign
    textContent, or escape every interpolation at a single choke point
  • Treat all contract-returned strings as untrusted input, including display_name, market
    question, and market image_url
  • Validate image_url against an allowlist of schemes -- reject javascript: and data:
    outright -- before it is ever placed in a src or href
  • Add the Content-Security-Policy header, so a missed escape is contained rather than fatal
  • Add length and character validation to display_name in referral_registry as defence in
    depth, so the chain stops storing payloads at all
  • Add a regression test that renders a hostile display name and asserts no script executes

Why this is critical

This is a wallet-draining vulnerability that any user can plant, that persists forever, and that
the two most-requested frontend features will activate. It should be fixed before the
leaderboard and referral work lands, not after.

Activity

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

    area: frontendWeb client, wallet, and user experiencearea: securitySecurity engineering and adversarial analysisbugSomething isn't workingpriority: criticalCritical to protocol safety or mainnet readiness

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions