My AI trading tool would sign a wallet drain as a login challenge A developer maintaining BagOS, an MCP server that lets AI agents trade on the Solana launchpad Bags, disclosed that a wallet-draining exploit bypassed all of the tool's spend guardrails by combining two flaws: the server loaded .env configuration from whatever project folder was open, and the bags_authenticate login tool blindly signed any challenge bytes it received. Because a Solana transaction signature is an ed25519 signature over the serialized message, a malicious repo could point BAGS_API_URL at an attacker-controlled endpoint that returned a balance transfer as the "challenge," yielding a broadcastable signature without invoking any write tool. Version 3.0.0 is a breaking release that requires an absolute BAGS_ENV_FILE path, pins the auth endpoint to https on bags.fm, and rejects any challenge that deserializes as a Solana transaction. BagOS is an MCP server I maintain. It lets an AI agent read token data on Bags, a Solana launchpad, and, if you configure a wallet, trade and claim creator fees. I built it around one idea: a model should be able to propose a spend but never complete one on its own. The first call to a write tool signs nothing. It returns a preview and a single-use token bound to the exact arguments. Every trade is capped. Every transaction is simulated before it's signed. This week an outside review showed that none of that mattered. There was a way to drain the wallet without touching a single write tool. It needed two flaws. Either one alone was harmless. 1. The server trusted the folder you had open. MCP clients such as Claude Code start a local server in the current project folder. BagOS called dotenv.config , which reads .env from the working directory. So any repository you opened could supply configuration you never set, including BAGS API URL , the endpoint the login tool talks to. 2. The login tool signed whatever it was given. bags authenticate proves you own a wallet: it fetches a challenge from the auth endpoint, signs it, and trades the signature for an API key. It signed the challenge bytes without checking what they were. On Solana, a transaction signature is an ed25519 signature over the transaction's serialized message. So if the "challenge" is a transaction message, the signature the tool sends back is a valid signature for that transaction. Whoever receives it can broadcast it. Put them together. A repo ships a .env that points BAGS API URL at a server its author controls. You open the repo, and your agent calls bags authenticate , maybe because a README told it to. The fake endpoint returns a transfer of your balance as the challenge. The tool signs it and sends the signature to that server. The login tool wasn't a write tool, so none of the guardrails applied: no token gate, no cap, no preview, no confirmation. My docs even said "Signing a challenge is not signing a transaction." Before the fix, that wasn't true. The suite had 100% line, branch and function coverage, enforced in CI. Every line of the auth tool was tested. The tests checked that it fetched a challenge, signed it and exchanged it, and it did all of that correctly. Coverage measures which lines run. It says nothing about which inputs you assumed were safe. My tests used a well-behaved endpoint and a config I wrote myself, because I had never asked who else could write that config or what else could arrive as a challenge. The missing check wasn't untested; it had never been written. 3.0.0 is a breaking release, because it changes how configuration loads. The server no longer reads .env from the working directory. You name a file explicitly, and the path must be absolute: js const explicit = env "BAGS ENV FILE" ?.trim ; if explicit { // A relative path resolves against the working directory, which is the // exact thing this function exists not to trust. if isAbsolute explicit { console.error / "refusing BAGS ENV FILE=...: it must be an absolute path" / ; return "refused-relative"; } If a .env is sitting in the working directory, the server says it's ignoring it, on stderr. The login tool now signs only Bags' exact sign-in text, with the nonce from the same init response. It also refuses anything that decodes as a Solana transaction, and anything that isn't printable text: export function isTransactionMessage bytes: Uint8Array : boolean { try { const message = VersionedMessage.deserialize bytes ; return Buffer.from message.serialize .equals Buffer.from bytes ; } catch { return false; } } The auth endpoint is pinned to https on bags.fm unless the operator sets BAGS ALLOW CUSTOM API URL=true . The model can no longer choose the keypair path either. I drafted a private GitHub security advisory, fixed the bug on a temporary private fork, and merged it from the advisory page. Then: One trap for anyone doing this with release-please: merging from the advisory page squashes the private fork into one commit titled "Merge commit from fork." That isn't a conventional commit, so release-please computed a patch version for a breaking change. I caught it by checking the release PR before merging, and fixed it by pushing an empty commit that restated the breaking change. Hours later, I had the review check my launch plan against the code, and it turned up a second gap. The spend caps checked the amount the agent asked for. But the swap transaction that actually gets signed is built by the Bags API, and nothing compared the two. The cap bounded the request, not the signature. 3.0.5 closes that. Before signing, BagOS reads the wallet's SOL balance, asks the simulation for the balance afterwards, and refuses if the difference is more than the approved amount plus 0.01 SOL for fees and rent. A fee claim approves nothing, so it may cost fees only. If the simulation doesn't report the balance, the transaction isn't signed. The check is required on every path, so no transaction can skip it. These are documented in SECURITY.md rather than hidden: --http serves /mcp on 0.0.0.0 . Don't run it with a funded wallet. The stdio default is unaffected. Links: repo https://github.com/edycutjong/BagOS · advisory https://github.com/edycutjong/BagOS/security/advisories/GHSA-g679-3wq7-mh3m · SECURITY.md https://github.com/edycutjong/BagOS/blob/main/.github/SECURITY.md · npm https://www.npmjs.com/package/bagos-mcp-server Earlier I wrote about a different BagOS bug, where the write tools reported success without signing anything: I shipped an MCP server that reported success without signing anything https://dev.to/edycutjong/i-shipped-an-mcp-server-that-reported-success-without-signing-anything-6oh . I build safety layers for AI agents that move money. I'm open to remote work.