Back to Feed
article 3m read

Session tool server gives a fake token one organization's data if you follow its README

A tool server for an AI assistant was committed whose queries all filter by organization, but a failed token falls through to an environment default that stays on unless one exact mode string is set, and none of the deploy recipes set it.

Article DepthPractice

Flow

Rules for picking the organization

  1. 1Bad or missing login

    Answer with a 401. A token with no organization gets one too.

  2. 2Dev mode

    A flag you turn on by name, listening only on your own machine.

  3. 3CORS

    Allow only the callers you expect.

  4. 4Tool helper

    No default. No organization means an error.

  5. 5Tool inputs

    Never ask the model for an organization.

  6. 6Tests

    Start with failures: no token, bad token, empty environment.

Committed: a server that lets an AI assistant read recorded coding sessions. If you deploy it the way its README says, it can answer a bad token with one organization's data.

One big commit

It was one big commit: six read-only tools, an auth module, a shared package and long setup guides. I suspect an agent wrote most of it. I committed it, so it's mine. The only test checks nothing.

The queries filter by organization

Every query filters by organization, and the value is passed in safely. The database side is fine. The trouble is in the code that picks the organization.

Three ways in, no way out

The server tries three things in order: a login token, a dev header that needs a flag, and a default organization from the environment. The default is used when the mode isn't set to production.

if (claims) return claims; // bad token? keep going
if (env.FALLBACK_TEAM && env.APP_MODE !== "live") return fallback();

A bad token doesn't stop the request. It just moves on. So a fake token, an old token and no token all reach the default. And "not production" is also true when nobody set the mode at all.

The token checker makes it worse. A real token with no organization in it gets dropped, and the request lands in the default organization anyway.

The second default

The helper that runs each tool also falls back to the default when no organization came in. It only refuses if no default is set.

The README walks you straight into it

Every hosting recipe in the README sets a default organization. None of them sets the mode to production. Unless the host sets the mode on its own, follow one exactly and anyone on the internet can read that organization's sessions.

Wildcard CORS and the loopback idea

The server lets any website call it. So even a dev copy on your own machine might be readable from a page in your browser. Keeping dev mode on your own machine isn't enough by itself.

Two authenticators, and the server picked the loose one

The auth module had a stricter checker with no default. The server wrote its own looser one and used that. If you review the strict one, you'll think the system is safe.

The shared package trusts a header

The shared package takes the organization from a header anyone can set. One tool also asks the model to fill in the organization, which teaches it the caller gets to choose.

The shape I want instead

If you're building something like this, these are the rules I'd hold it to:

  • A failed or missing login is a 401. A token without an organization is a 401 too.
  • Dev mode is a flag you turn on by name, and it only listens on your own machine.
  • Only the callers you expect are allowed by CORS.
  • The tool helper has no default. No organization means an error.
  • No tool asks the model for an organization.
  • There's one auth function, not two.
  • Tests start with the failures: no token, bad token, empty environment.

What the code can't tell me

Fallback logins do print a warning, so a log would show one. Whether a copy ever ran in public, though, is not something the code can tell me.

Keep reading

Pick up a connected idea or branch into a nearby one.

5 paths forward