Skip to content

fix findings from the casa security self-check - #147

Merged
hkonsti merged 8 commits into
mainfrom
hkonsti/pr-86-recreation
Oct 23, 2026
Merged

hkonsti merged 8 commits into
mainfrom
hkonsti/pr-86-recreation

Conversation

@hkonsti

@hkonsti hkonsti commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Security self-check for Google's CASA assessment (AL1), run again on today's main, which has moved a long way since #86. It covers the repository only: no requests were made to the hosted service, and DAST is left for a staging instance. This replaces #86.

Scanned with

  • Semgrep 1.136.0: p/owasp-top-ten, p/typescript, p/nodejs, p/secrets, plus p/default, p/react, p/jwt. 14 findings: one fixed here (gcm-no-tag-length). The rest are mutable action tags in CI (filed) or false positives: regexes built from literal tag and attribute names, a fixed index.html for sendFile, a test fixture and a setup script.
  • gitleaks 8.30.1 on the git history and the working tree. Every match is a false positive: the documented dev key in .env.example, PostHog's public ingestion key, old mock data and local build caches. Nothing new on this branch.
  • bun audit: two transitive advisories. ip-address is fixed here. deepmerge-ts 7.1.5 is pinned by @prisma/config and needs Prisma 7; html-to-text already gets 8.0.2.
  • A hand review against the ASVS items in the verification prep, of everything that landed since fix findings from the casa security self-check #86: organizations and members, invitations, shared and own connections and agents, the vault, tasks, the OAuth server, sessions and cookies, the origin check, the SSRF guard on custom MCP servers, MCP OAuth and favicons, the SQL connector, and XSS in the web app. Plus the repo's security-review skill on this branch, which found nothing.

Fixed here

  • An agent allowed only read-only queries could write to the database. The SQL check read quotes and comments its own way. Postgres ends 'x\' at the backslash and has $tag$ strings, and MySQL has # comments. So SELECT 'x\'; COMMIT; DELETE FROM users; --' passed as one statement, the COMMIT ended the read-only transaction, and the DELETE ran. Any semicolon before the trailing ones is now refused.
  • A member who left kept access to the organization's shared connections. They could still sign in with the passwords of the shared agents they had made. Leaving, being removed or deleting the account now gives those agents a password nobody knows and ends their sessions. The agents stay, and so do the MCP clients other members signed in with them. The owner gives an agent a new password to use it again. The members page and the docs said the old password kept working; both are updated.
  • The api now sends nosniff, Referrer-Policy, X-Frame-Options: DENY, frame-ancestors 'none' and, over https, HSTS on every response. It also stops sending X-Powered-By. The docs and the website get the same headers from render.yaml.
  • The AES-GCM tag length is pinned when decrypting stored secrets.
  • A malformed Basic header on /oauth/token now gets invalid_client instead of a 500.
  • Tool params accept only a tool's own param names, not inherited ones like constructor.
  • ip-address 10.7.4 (GHSA-rxp7-6hxx-x8q5), within the range imapflow's socks asks for.

Already on main since #86, so dropped from here: Google sign-in's tokens are no longer stored, human session tokens are hashed at rest (#125), and accounts can be deleted in the app (#108).

Filed for later

  • Rate limiting on sign-in, agent login, the OAuth endpoints and MCP batches
  • Pruning of the activity log and of expired sessions, codes and grants
  • A full content security policy for the web app
  • The SSRF guard for the email connector's IMAP and SMTP hosts
  • Logging fewer error causes, and less of the activity log's titles and SQL
  • Pinning CI actions and the Docker base image
  • Prisma 7, for deepmerge-ts
  • Refresh token and code reuse detection in the OAuth server, revoking the grant on replay
  • A login CSRF nonce on magic links, so a link only signs in the browser that asked for it
  • Reading only the __Secure- session cookie over https, ideally as __Host-
  • Showing an error page rather than redirecting for /oauth/authorize requests from a client that never completed a grant: anyone can register a client, so today it is an open redirect
  • An AgentSession.userId, so signing out a member can also end sessions they opened on shared agents they didn't make
  • DAST with OWASP ZAP and the Fluid Attacks SAST, run by the maintainer against staging

Typecheck, lint, build and all 1,175 tests pass. The member removal change was also checked against the local database.


Open workspace in Conductor

Only the web app's files had nosniff, a referrer policy and a framing rule; the api's JSON, its
redirects and its 404s had none, and Express announced itself. Set them once in front of
everything, deny framing everywhere (nothing frames the web app) with frame-ancestors as well,
and add HSTS where APP_URL is https, since neither Render nor Caddy sends it.
The docs and the website are static sites on Render, which adds none of nosniff, a referrer
policy, a framing rule or HSTS. Set them in the blueprint, as the app sets them itself.
Without authTagLength, node's decipher takes a tag as short as four bytes, which is far easier
to forge; Bun already refuses one, so this states the rule instead of relying on the runtime,
and a test keeps it. Flagged by semgrep's gcm-no-tag-length.
…en request

decodeURIComponent throws on a malformed percent sequence, which escaped the Do block and became
a 500 with a logged stack. Treat it as no credentials, so the client gets invalid_client.
properties[key] found constructor and toString on the prototype, so a tool appeared to take them;
parseRecordValues refused them later, but the check meant to say which params a tool takes
should say it itself.
imapflow reaches it through socks, as does the shadcn cli. 10.7.4 fixes GHSA-rxp7-6hxx-x8q5 and
is within the range both ask for.
Removing a member, or their leaving or deleting their account, left the shared agents they made
as they were: they had seen each one's password, so they could still sign in as it and use every
shared connection with it until someone reset it by hand. Give those agents a password nobody
knows and end their sessions as the member goes; the agents stay, the MCP clients other members
signed in with them keep working, and the owner hands them a new password to sign in with again.
…a read

The check for a second statement read quotes and comments its own way, which neither database
does: Postgres ends 'x\' at the backslash and has $tag$ strings, and MySQL has # comments. So a
query like SELECT 'x\'; COMMIT; DELETE FROM users; --' passed as one statement, the COMMIT
ended the read-only transaction, and the DELETE ran, with only Run queries allowed. Refuse any
semicolon but the trailing ones instead: no statement follows another without one, and an agent
can write a query without a semicolon in its strings.
@hkonsti
hkonsti merged commit 9dbfcc9 into main Oct 23, 2026
2 checks passed
@hkonsti
hkonsti deleted the hkonsti/pr-86-recreation branch October 23, 2026 18:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant