Use Security → Report a vulnerability on this repository. The advisory is private: it is visible only to the reporter and the maintainer. Do not open a public issue, and do not file a pull request that shows the exploit.
Expect an acknowledgement within a week. If a fix is possible, one ships before the advisory becomes public, and the release notes credit the reporter unless they ask not to be.
| Version | Supported |
|---|---|
| 0.1.x | yes |
| anything older | no — update, and rebuild |
This SDK is unaudited, and so are the contracts it drives and the decision service it verifies. A bug in either of those is a bug in whatever this library concludes from them; read their security pages too.
send() builds a transaction, signs it, and signs whatever authorisations the
node asks for. So the questions are: what can a caller of this SDK be tricked
into, and what can the infrastructure around it be tricked into?
- You are handing it a signer. The SDK never stores a key and takes one only
as a
Signer, but it does callsign()on digests chosen during the send path — the transaction'ssignatureBase(), and the preimage hash of each authorisation entry. This is ordinary for an SDK and it is still the sharpest edge here: give akeypairSignerfor an admin key to a process that also fetches and runs other people's code, and that code can spend the admin. - The authorisation entries come from the node, and only partially checked.
signAuthorizationsverifies that an entry asks the right address to authorise — it refuses to sign for anyone other than its own signer, which is the check the underlyingauthorizeEntrydoes not perform. It does not verify that the entry'srootInvocationis the call you built. A hostile or compromised RPC can therefore obtain a signature authorising a different contract call, valid forauthLedgerWindowledgers (default 10) and bound to the network passphrase. If you cannot trust the endpoint, run your own;connect(url)acceptingallowHttpis for a local node, and the default is refusal. - The simulation result shapes the transaction. Footprint, required auth and
minResourceFeeall come back fromsimulateTransaction. An RPC can inflate the fee, return a footprint that makes the call fail, or answernot_foundtogetTransactionforever. None of that mints an unauthorised grant, which is why the design accepts it — but it does mean "the client is honest" is not the same as "the pipeline is". - A decision signature is a shared secret, and verification is optional.
verifyDecisionproves a decision came from a holder ofKEYGATE_SIGNING_KEYand was not edited in transit. Anyone with that key can forge decisions for any holder, and there is no rotation here. Equally: a caller that skips verification — or callsdecisionIsUsablewithout verifying — is trusting whatever its proxy returned. Treat a missingx-keygate-signatureheader as a refusal, because an unsigned body is what a downgrade attack looks like. - Decoders fail loudly, but they can be wrong quietly in one place. Every
as*helper throws on an unexpected tag, which is the safe direction. The exception is a contract upgrade that changes what a valid-looking tag means:EFFECT_DISCRIMINANTsaysallowis0, and ifkeygate-policyever reorders itsEffectenum,effectFromDiscriminantinverts every rule with no type error and no thrown exception. That table is a wire format. codeFromHostErrorparses someone else's error string. It looks forError(Contract, #n)in prose the Soroban host emits. If that format changes, the result iscode: undefinedand the raw text indetail— a lost matching convenience, not a wrong allow. It is listed here because string-parsing a dependency is a coupling that should be visible.
- This SDK signing an authorisation for an address other than its signer's.
verifyDecisionaccepting a signature for a request other than the one passed in, in either direction (wrong amount, wrong holder, wrong reason).- A decoder returning an allow-shaped value for a ScVal the contract did not
return —
asBoolon a map,asOptionreading a present value as absent. - A builder producing arguments that encode to the wrong Soroban type in a way that lets a call succeed where the contract meant to refuse it.
- Key material written to a log, an error message, or a returned structure.
- Anything that lets a transaction be submitted twice for one
send()call without the caller knowing (seeCommitTimeoutError, which exists precisely to not guess).
- A hostile RPC doing hostile RPC things: fee inflation, withheld transactions, fabricated simulation errors. Item 3 above; the mitigation is an endpoint you run.
- A compromised or careless holder of
KEYGATE_SIGNING_KEY. - Bugs in
@stellar/stellar-sdk, in Soroban, or in the keygate contracts. - Wallet UX: this library cannot stop a caller from signing a digest it should not
have been asked to sign, any more than
stellar-sdkcan. - Versions older than the table.
This SDK is the layer where on-chain intent is turned into signatures, and the
assumption it makes is that the contracts are the authority and the node is a
messenger. Everything a caller learns about capability state comes back through
simulation, so a lying node can mislead but cannot authorise: the contract checks
require_access against its own storage when the transaction executes, and the
off-chain mirror in keygate-server is reconstructible from public events. What
the SDK can cause is a signature that should not exist — hence the holder check
before any authorisation is signed, the refusal to guess on a timed-out commit,
and a decision verification path that returns false for every failure mode
rather than distinguishing them for an attacker's benefit.