Security & UX
May 27, 2026

The Tool Reviews Itself

The most honest test of a code review tool is whether it survives its own review. Code Reviewer reviewed Code Reviewer. Gopher got the same treatment. Neither passed cleanly.

After closing thirteen findings in DrawmaticAI, the obvious next question was: what about the other apps? The answer was to run the same AI security review against Code Reviewer and Gopher. The results were uncomfortable in the expected ways, and revealing in a few unexpected ones.

The Patterns That Keep Coming Back

Two findings appeared in every app I have reviewed so far. First: hardcoded admin email. Every app had my personal email embedded in database.py as a direct string comparison, granting admin on match. The fix is always the same -- read it from a SUPERADMIN_EMAIL environment variable, compare case-insensitively, skip the check entirely if the variable is unset. But in Gopher, the hardcoded check appeared in two places: once in database.py where the user is created, and once in auth.py's require_admin function. Fixing the first missed the second. Now I check both files every time.

Second: ephemeral secret keys. When a required environment variable like FERNET_KEY or SECRET_KEY is unset, the temptation is to generate a random one and keep going. Every session token becomes invalid on restart. Every encrypted credential becomes unreadable. The fix is to fail at startup with a clear error. Not degrade silently. Not generate a placeholder. Stop. The application should never run in a state where its security invariants do not hold.

The One That Surprised Me: Redirect-Chain SSRF

Both apps had SSRF mitigation in place: validate that the URL scheme is http or https before making the request. That feels sufficient. It is not.

httpx.AsyncClient follows redirects by default. An attacker-controlled server can return a 302 to http://169.254.169.254/ or any internal address. The scheme check passes on the original URL. The redirect carries the request to an internal host. The validation was bypassed without touching it.

The fix is one word: follow_redirects=False. Every outbound httpx client in the suite now has it. It is easy to miss because the attack requires a cooperating external server, which feels hypothetical. In a tool that executes AI-directed HTTP calls against registered endpoints, it is not hypothetical at all.

While fixing SSRF, I also found that Gopher's app-discovery function was storing the endpoint URL it received in remote JSON responses without validating it first. An app could return any URL as its endpoint and Gopher would persist it and later use it for outbound calls. That endpoint now gets the same scheme check before it is stored.

JWT Issuer: The Trap in the Default

Gopher's verify_app_token accepted an expected_issuer parameter, but it defaulted to None. When the parameter was None, the issuer claim was never checked. The function signature looked like it supported issuer validation. The behavior was that issuer validation only happened if the caller remembered to ask for it.

The iss claim is now in the required claims list and the check is unconditional. The default changed from None to "gopher". If you want to accept a token from a different issuer, you pass the parameter explicitly. Secure by default; the opt-in is for trust expansion, not for security enforcement.

PII in the Logs

The chat endpoint was logging the first 200 characters of every LLM response and the first 300 characters of every tool result. LLM responses in a personal OS assistant contain meeting summaries, email contents, calendar entries, and task descriptions. Tool results from inbox0 and Summarizer contain the same. Logging them verbatim, even with truncation, means a copy of personal data ends up in the application log file.

The fix is to log metadata, not content. Response length instead of response text. Key names instead of values. The logs still tell you what happened -- which tool ran, whether it succeeded, how large the response was -- without persisting what was said.

Streaming UI Error Handling

Code Reviewer has five distinct streaming review flows: file upload, GitHub file, GitHub branch, PR diff, and folder review. Each flow opens a fetch stream and reads chunks. Each had the same structural bug: the catch block checked for AbortError (the user clicked Stop) and re-threw everything else. Network errors, connection drops, Safari's TypeError: Load failed -- all became unhandled promise rejections. The badge stayed on "Streaming." The button stayed disabled. The UI was stuck.

The fix was consistent across all five flows. Any error that is not an intentional abort shows an "Error" badge and a "connection lost" toast. The currentAbortController is nulled in every exit path -- both the outer catch that handles a failed initial fetch and the inner finally block that handles mid-stream failure. A non-null controller was the gate that prevented history items from loading, and it was staying set long after the stream had ended.

Navigating to the admin panel while a review was running killed the stream entirely, because the admin link was a standard <a href="/admin"> that caused a full page navigation. It now opens in a new tab. The brand logo and logout button ask for confirmation before navigating away. The browser's native beforeunload dialog catches anything else.

The Pattern

After three security reviews across four apps, the pattern is clear. The dangerous bugs are not the exotic ones. They are the defaults that were convenient during development and never revisited: the hardcoded email that saved a config step, the key that generated itself rather than failing, the redirect that the library followed silently, the issuer check that only ran when you remembered to ask. Every one of them had a plausible reason to exist when it was written. None of them should have survived to production.

The uncomfortable part of using Code Reviewer on its own codebase was not finding bugs in it. It was recognizing exactly when and why each shortcut was taken.

#Security #CodeReviewer #Gopher #SSRF #JWT #MyOwnLifeOS #BuildInPublic
Ovais Quraishi