Summary
Caatinga builds stellar argv arrays by hand and never checks that user/config-supplied tokens are not themselves flags. execa is used without a shell, so this is not shell injection — but it is argument injection into the Stellar CLI, and every affected path ends in a signed transaction.
Unvalidated values that land in argv positions:
| Value |
Source |
Lands at |
target.method |
ctg invoke <contract>.<method> |
after --, as the contract method name (invoke-contract.ts:63) |
| method arg keys |
--<key> in ctg invoke/read, deployArgs/postDeploy.args in config |
--${toSnakeCaseFlag(key)} (format-cli-args.ts:22) |
| method arg values |
same |
positional value right after the flag (format-cli-args.ts:22) |
upgradeMethod / wasmArg |
UpgradeContractOptions |
-- <upgradeMethod> --<wasmArg> (upgrade-contract.ts:133-134) |
source |
--source, CAATINGA_SOURCE |
stellar keys address <source> positional (resolve-source-address.ts:19) and --source-account <source> |
hook.method |
postDeploy in caatinga.config.ts |
after -- (run-post-deploy.ts:203) |
Two concrete gaps:
1. assertSorobanSymbol is dead code. packages/core/src/soroban/assert-soroban-symbol.ts implements exactly the right check (/^[A-Za-z0-9_]{1,32}$/), but grep shows its only non-test reference in the whole repo is the re-export in browser.ts:8. Neither parseInvokeTarget (invoke-target.ts:10-22, which only splits on .) nor run-post-deploy nor upgrade-contract calls it. Method names reach the CLI completely unchecked.
2. assertSafeSourceAccount doesn't reject flag-shaped aliases. validate-source-shape.ts rejects secret keys (S…), seed phrases (whitespace) and public keys (G…) — good — but a source of --config-dir or -h passes and is handed to stellar keys address <source> as a positional, where clap reads it as an option.
Failure scenario
ctg invoke counter."--some-flag" --network mainnet --source deployer (or a postDeploy entry whose args value is --other_param) produces:
stellar contract invoke --id C... --source-account deployer --network mainnet -- --some-flag ...
Everything after -- is parsed by the clap subcommand the Stellar CLI generates from the contract spec, so an attacker-influenced or typo'd value is interpreted as a different parameter of the same method rather than as the value of the intended one — silently changing what gets signed. The Stellar CLI also exposes --<arg>-file-path style options in that position, which widens the impact from "wrong argument" to "argument read from an arbitrary file".
The realistic threat model here is not a remote attacker: it is a config file, a CI variable, or an npm run script that someone else contributed. The point is that the coordination layer offers no validation between those inputs and a signed mainnet transaction.
Suggested fix
- Call
assertSorobanSymbol on target.method in parseInvokeTarget, on hook.method in run-post-deploy, and on upgradeMethod/wasmArg in upgrade-contract.
- In
formatNamedCliArgs, reject keys that don't match /^[A-Za-z_][A-Za-z0-9_]*$/ and values that start with -, or pass values as --key=value so a leading dash cannot be re-parsed as a flag.
- Extend
validateSourceShape to reject any source starting with -.
Note: validate-source-shape.ts:5 also rejects any alias starting with a capital S as a "secret key" — --source Staging is refused. Worth tightening to the full S[A-Z2-7]{55} strkey shape in the same pass.
Scope: audit was read-only, no code changed.
Summary
Caatinga builds
stellarargv arrays by hand and never checks that user/config-supplied tokens are not themselves flags.execais used without a shell, so this is not shell injection — but it is argument injection into the Stellar CLI, and every affected path ends in a signed transaction.Unvalidated values that land in argv positions:
target.methodctg invoke <contract>.<method>--, as the contract method name (invoke-contract.ts:63)--<key>inctg invoke/read,deployArgs/postDeploy.argsin config--${toSnakeCaseFlag(key)}(format-cli-args.ts:22)format-cli-args.ts:22)upgradeMethod/wasmArgUpgradeContractOptions-- <upgradeMethod> --<wasmArg>(upgrade-contract.ts:133-134)source--source,CAATINGA_SOURCEstellar keys address <source>positional (resolve-source-address.ts:19) and--source-account <source>hook.methodpostDeployincaatinga.config.ts--(run-post-deploy.ts:203)Two concrete gaps:
1.
assertSorobanSymbolis dead code.packages/core/src/soroban/assert-soroban-symbol.tsimplements exactly the right check (/^[A-Za-z0-9_]{1,32}$/), butgrepshows its only non-test reference in the whole repo is the re-export inbrowser.ts:8. NeitherparseInvokeTarget(invoke-target.ts:10-22, which only splits on.) norrun-post-deploynorupgrade-contractcalls it. Method names reach the CLI completely unchecked.2.
assertSafeSourceAccountdoesn't reject flag-shaped aliases.validate-source-shape.tsrejects secret keys (S…), seed phrases (whitespace) and public keys (G…) — good — but a source of--config-diror-hpasses and is handed tostellar keys address <source>as a positional, where clap reads it as an option.Failure scenario
ctg invoke counter."--some-flag" --network mainnet --source deployer(or apostDeployentry whoseargsvalue is--other_param) produces:Everything after
--is parsed by the clap subcommand the Stellar CLI generates from the contract spec, so an attacker-influenced or typo'd value is interpreted as a different parameter of the same method rather than as the value of the intended one — silently changing what gets signed. The Stellar CLI also exposes--<arg>-file-pathstyle options in that position, which widens the impact from "wrong argument" to "argument read from an arbitrary file".The realistic threat model here is not a remote attacker: it is a config file, a CI variable, or an
npm runscript that someone else contributed. The point is that the coordination layer offers no validation between those inputs and a signed mainnet transaction.Suggested fix
assertSorobanSymbolontarget.methodinparseInvokeTarget, onhook.methodinrun-post-deploy, and onupgradeMethod/wasmArginupgrade-contract.formatNamedCliArgs, reject keys that don't match/^[A-Za-z_][A-Za-z0-9_]*$/and values that start with-, or pass values as--key=valueso a leading dash cannot be re-parsed as a flag.validateSourceShapeto reject any source starting with-.Note:
validate-source-shape.ts:5also rejects any alias starting with a capitalSas a "secret key" —--source Stagingis refused. Worth tightening to the fullS[A-Z2-7]{55}strkey shape in the same pass.Scope: audit was read-only, no code changed.