Code Audit is live!  Try it now
Blog

"Please perform a comprehensive security audit" - and why it doesn't work

How Claude Code or Codex won't secure your application, and why AISafe found 7 CVEs in a file hosting app where they couldn't.

Šimon Šustek

Šimon Šustek

March 16, 2026

Fineas Silaghi

Fineas Silaghi

March 16, 2026

"Please perform a comprehensive security audit" - and why it doesn't work

Introduction

Hi there! We are AISafe Labs, a crew of security researchers with only one goal: making security accessible to everyone!

We've all witnessed the recent rise of LLMs that has been nothing short of revolutionary. We're reaching a point where the power of a single prompt surpasses any expectation. From generating entire codebases to replacing entire workflows, the bar keeps getting raised. Building this much trust in the power of a prompt naturally raises the question: "Can you secure your application with a single prompt?"

In this article, we put that question to the test. We will assess how far a single prompt can take you when it comes to securing a real application, then compare that to a more targeted prompt armed with domain knowledge and specific instructions, and finally, stack both against AISafe, our specialized source code auditing platform. Same codebase, same goal, will the difference be significant or negligible?

ProductFindingsVulnerabilities with direct impactWeaknesseses without direct impactAccepted riskFalse positives
Claude Code2418114
Claude Code w Skills2105214
Claude Code /security-review10100
Codex42200
Codex Security44618811
AISafe209470

Let's begin! 🍿


The Target

Gokapi is an open source file hosting application with around 2.7k stars on GitHub, 1.5M Docker pulls, and roughly 35k lines of code (Go & JavaScript).

Preview of Gokapi app

It sits in a sweet spot for our case study: large enough to present a complex threat model and with modern web app features such as file handling, API keys, and user permissions with different access levels, yet small enough to audit in a reasonable time.

All experiments were run against commit a7c4273b819b8a48f85b866e1803632c089f60a2, pushed on March 1st, 2026.


Anthropic

Claude Code

Our first candidate is Claude Code, running Opus 4.6 with high effort. We are going to start with the most simple setup possible: a single prompt, just the kind that it's being recommended on social media every other day.

To begin, we drop Claude Code into the Gokapi folder, and type "Please perform a security audit of this repository". It will spin up a few agents and start looking at different parts of the codebase:

A few minutes later, we are already looking at 24 findings 👀

4 criticals? Let's break them down!

The first two are "SHA-1 Password Hashing" and "Zero-Filled Nonces in AES-GCM Encryption".

While Gokapi does use SHA-1 for password hashing, and sure, that's definitely not a great idea, still, it is a weakness rather than an exploitable vulnerability. A Low severity finding at best.

The "zero-filled nonce" finding sounds scary until you take a closer look and realize that the nonce gets handed off to the sio-go library, which appends its own nonce on top, and the server also uses per-file keys. Again, the real-world impact is practically none.

The other two critical findings flag "XSS in templates" and an "open redirect". Unfortunately both are false positives: the templates rely on Go's html/template package which handles sanitization out of the box, and on the other hand the redirect URL is only ever set by the instance admin.

Four critical vulnerabilities reported, but no real security issues were identified.

Moving on to the high impact findings. Three of the seven are about missing cookie attributes and security headers like HttpOnly, Secure, SameSite, Content-Security-Policy, and X-Frame-Options. Not a bad report, but again, not really vulnerabilities either. Another finding flags missing CSRF protection, which has had generally pretty limited impact ever since SameSite=Lax became the default.

Finally, there is one finding that points us to something actually vulnerable: a "Header Injection" issue, where file.Name is potentially attacker-controlled:

w.Header().Set("Content-Disposition", "attachment; filename=""+file.Name+""")

However, the impact is overstated. Go validates headers and blocks CRLF injection, so the possible corruption is limited only to the header.

Lastly, the final two high impact findings describe header-based auth, which is intentional by design, and a missing TLS configuration that turns out to be another false positive.

The mediums are all theoretical with no meaningful real-world impact.

Full report

CRITICAL

1. SHA-1 Password Hashing

internal/configuration/Configuration.go:224-236

Passwords are hashed with SHA-1 + salt, which is cryptographically broken. SHA-1 can be cracked at billions of hashes/second on commodity GPUs.

hash := sha1.New() hash.Write(pwBytes) return hex.EncodeToString(hash.Sum(nil))

Fix: Migrate to bcrypt or Argon2id.

2. Zero-Filled Nonces in AES-GCM Encryption

internal/encryption/Encryption.go:156, 168, 195, 202, 209, 216

Nonces are always initialized as zero-filled byte arrays. Reusing a nonce with the same key in GCM mode completely breaks confidentiality and authenticity.

nonce := make([]byte, stream.NonceSize()) // always zeros

Fix: Generate random nonces via crypto/rand.Read().

3. XSS via Unescaped Filenames in Templates

Multiple templates embed user-controlled filenames without proper escaping:

FileContext
web/templates/html_redirect_filename.tmpl:5-16<title>, <meta> tags
web/templates/html_redirect_filename.tmpl:24,29JavaScript string and <a href>
web/templates/html_download.tmpl:25,126HTML <h4> and JavaScript createWriteStream()
web/templates/html_admin.tmpl:78,227Admin panel table cells
web/templates/html_uploadrequest.tmpl:41-63onclick handler attributes

A file named <script>alert(1)</script>.txt or similar could execute arbitrary JavaScript, leading to session hijacking or admin account takeover.

4. XSS via RedirectUrl in Meta Refresh

web/templates/html_index.tmpl:2

<meta http-equiv="Refresh" content="0; URL={{.RedirectUrl}}">

If RedirectUrl is attacker-controlled, javascript: URIs can execute code.


HIGH

5. Session Cookie Missing HttpOnly and Secure Flags

internal/webserver/authentication/sessionmanager/SessionManager.go:85-92

c := &http.Cookie{ Name: "session_token", Value: sessionString, Expires: expiry, SameSite: http.SameSiteLaxMode, // Missing: HttpOnly, Secure }

Session tokens are accessible to JavaScript (XSS can steal them) and may transit over plaintext HTTP.

6. File Password Cookie Missing All Security Flags

internal/webserver/Webserver.go:1095-1101

The cookie storing the file's password hash has no HttpOnly, Secure, or SameSite attributes.

7. No CSRF Protection

No CSRF tokens are implemented anywhere. All state-changing POST endpoints (file upload, user management, password change, API key creation) are vulnerable to cross-site request forgery.

8. Header Injection via Content-Disposition

internal/webserver/headers/Headers.go:15,17

w.Header().Set("Content-Disposition", "attachment; filename=""+file.Name+""")

Filenames with " or can break out of the header value, enabling HTTP response splitting.

9. Missing HTTP Security Headers

No global middleware sets standard security headers:

  • Content-Security-Policy
  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • Strict-Transport-Security
  • Referrer-Policy

10. Header-Based Auth Trusts Proxied Headers Without Validation

internal/webserver/authentication/Authentication.go:125-134

userName := r.Header.Get(authSettings.HeaderKey) // direct trust

If the reverse proxy is misconfigured or bypassed, any client can forge the header and authenticate as any user.

11. No TLS Configuration (Allows TLS 1.0/1.1)

internal/webserver/Webserver.go:141-158

http.Server has no tls.Config, defaulting to TLS 1.0. No cipher suite restrictions are applied.


MEDIUM

12. User Enumeration via Timing on Login

internal/webserver/authentication/Authentication.go:277-286

When a username doesn't exist, the function returns immediately without performing any hash comparison, creating a measurable timing difference.

13. File Password Comparison Not Constant-Time

internal/webserver/Webserver.go:1105-1116

if cookie.Value == file.PasswordHash { // vulnerable to timing attack

14. Weak Rate Limiting on Login

internal/webserver/ratelimiter/RateLimiter.go:34-38

Only allows 2 free attempts before a 3-second delay. No exponential backoff. No rate limiting at all on API authentication endpoints.

15. Config File Written with World-Readable Permissions

internal/configuration/Configuration.go:119

file, err := os.OpenFile(parsedEnvironment.ConfigPath, ..., 0644)

Config files containing encryption keys and password hashes are readable by all local users. Should be 0600.

16. Self-Signed Certificate Uses Hardcoded Serial Number

internal/webserver/ssl/Ssl.go:100

SerialNumber: big.NewInt(1), // should be random

17. Path Traversal Risk in ZIP Entry Names

internal/storage/FileServing.go:691

Filenames are written to ZIP entries without sanitization. A name like ../../etc/passwd could cause extraction outside the intended directory on the client side.

18. OAuth State Parameter Only 16 Bytes

internal/webserver/authentication/oauth/Oauth.go:51

Should be at least 32 bytes for adequate entropy.

19. Sensitive Errors Leaked to Clients

internal/webserver/Webserver.go:983, oauth/Oauth.go:90-110

Raw err.Error() strings are returned in API responses and OAuth error pages, potentially leaking internal paths, library versions, or stack info.

20. 30-Day Session Lifetime

internal/webserver/authentication/sessionmanager/SessionManager.go:17

Admin sessions last 30 days with no idle timeout. Consider 7-14 days with sliding window.


LOW

21. Auto-User Creation on OAuth Login

internal/webserver/authentication/Authentication.go:238-255

Users are auto-provisioned unless OnlyRegisteredUsers is set. Default should be restrictive.

22. Credentials Printed to Console During Setup

internal/configuration/setup/Setup.go:75-78

Setup credentials appear in plaintext in Docker logs / stdout.

23. SVG Injection in Expired File Image

web/templates/expired_file_svg.tmpl:3PublicName embedded without escaping.

24. Minimum Password Length Allows 6 Characters

internal/environment/Environment.go:66minValue:"6" is too short.


Summary

SeverityCountKey Issues
Critical4SHA-1 passwords, zero nonces, XSS (templates)
High7Cookie flags, CSRF, header injection, security headers, TLS config
Medium9Timing attacks, rate limiting, file permissions, error leakage
Low4Auto-provisioning, password policy, info disclosure

Top 5 Priorities for Remediation

  1. Replace SHA-1 password hashing with bcrypt/Argon2id
  2. Fix zero-nonce encryption (randomize all GCM nonces)
  3. Sanitize/escape all user-controlled data in templates (filenames, URLs)
  4. Add HttpOnly, Secure, SameSite to all cookies
  5. Implement CSRF protection on all state-changing endpoints

Summary:

At first glance, the results might look like a success. Twenty-four findings sound impressive, and the report looks thorough. But, once you dig in you will realize that while some of the findings are fix-worthy, most of them have little to no real impact.

We've tried several other prompt variations, but the results were largely the same.

Claude Code \w bug-class specific prompts

If you blame the generic prompt for the previous results, don't worry because in this experiment we will up our prompt game and try to hunt more specific: "Look for business logic bugs and permission problems".

This time we only got 10 total findings, and things are already looking more promising. The single critical finding points to a function that runs every hour to clean up file requests belonging to deleted users:

func cleanInvalidFileRequests() { users := getUserMap() for _, fileRequest := range database.GetAllFileRequests() { _, exists := users[fileRequest.UserId] if !exists { files := database.GetAllMetadata() for _, file := range files { if file.UploadRequestId == fileRequest.Id { } DeleteFile(file.Id, true) } database.DeleteFileRequest(fileRequest) } } }

There is an obvious typo here, but a pretty painful one. If it encounters a file request whose owner no longer exists, it will delete every single file from the application. That said, the trigger condition is narrow just as the comment sitting right above the function explicits:

// cleanInvalidFileRequests removes file requests and the associated files from the database if their associated owner is not a valid user. // Normally this should not be a problem, but if a user was manually deleted from the database, // this could cause issues otherwise.

This only fires when a user is deleted directly from the database, not through the application's logic. Still this is a real bug, just not as alarming as the title sounds. Medium impact would be a more appropriate classification.

Two other genuine findings also made it through:

  1. A user without UserPermGuestUploads permission can generate a temporary API key with ApiPermManageFileRequests and use it to create File Requests they should never have been able to create. A solid finding that breaks a real permission assumption.

  2. A copy-paste mistake in apiUploadRequestListSingle that checks UserPermDeleteOtherUploads on a function meant for listing, not deleting:

func apiUploadRequestListSingle(w http.ResponseWriter, r requestParser, user models.User) { request, ok := r.(*paramURequestListSingle) if !ok { panic("invalid parameter passed") } uploadRequest, ok := filerequest.Get(request.Id) if !ok { sender(w, http.StatusNotFound, errorcodes.NotFound, "FileRequest does not exist with the given ID") return } if uploadRequest.UserId != user.Id && !user.HasPermission(models.UserPermDeleteOtherUploads) { sender(w, http.StatusUnauthorized, errorcodes.NoPermission, "No permission to delete this upload request") return } result, err := json.Marshal(uploadRequest) helper.Check(err) _, _ = w.Write(result) }

A valid bug, though with limited security implications in practice.

The rest of the findings circle back to familiar territory: file passwords stored as hashed values in a short-lived browser cookie (which appears intentional), a missing HttpOnly flag, and a non-constant-time comparison.

Full report

Bug 1 — CRITICAL: cleanInvalidFileRequests() deletes ALL files

File: internal/storage/FileServing.go:844-859

func cleanInvalidFileRequests() { users := getUserMap() for _, fileRequest := range database.GetAllFileRequests() { _, exists := users[fileRequest.UserId] if !exists { files := database.GetAllMetadata() for _, file := range files { if file.UploadRequestId == fileRequest.Id { } // -- EMPTY BLOCK DeleteFile(file.Id, true) // Runs for EVERY file } database.DeleteFileRequest(fileRequest) } } }

Root Cause: The if file.UploadRequestId == fileRequest.Id block is empty. The DeleteFile call sits outside it and executes unconditionally for every file in the database.

Trigger: A file request's owner user is manually deleted from the database (or removed via database corruption). The hourly CleanUp() cycle calls cleanInvalidFileRequests(), which then deletes every file in the system.

Impact: Complete data loss — all files are marked expired and removed from storage (local filesystem or S3).

Evidence:

  • Empty if block: FileServing.go:851-852
  • Unconditional DeleteFile: FileServing.go:853
  • Correct pattern exists at models/FileRequest.go:36-43 (same comparison, logic correctly inside block)
  • Analogous cleanInvalidApiKeys() at FileServing.go:824-839 follows the correct pattern
  • No test coverage: zero matches for cleanInvalidFileRequests in test files
  • Called at startup via Main.go:69CleanUp(true)cleanInvalidFileRequests()

Fix: Move DeleteFile(file.Id, true) inside the if block:

if file.UploadRequestId == fileRequest.Id { DeleteFile(file.Id, true) }

Bug 2 — HIGH: Password hash leaked in download cookie

File: internal/webserver/Webserver.go:1096-1101

func writeFilePwCookie(w http.ResponseWriter, file models.File) { http.SetCookie(w, &http.Cookie{ Name: "p" + file.Id, Value: file.PasswordHash, // Raw SHA-1 hash sent to client Expires: time.Now().Add(5 * time.Minute), }) }

Root Cause: The raw password hash (SHA-1 with server-wide salt) is set as the cookie value. The hash IS the authentication token — isValidPwCookie() at line 1108 compares cookie.Value == file.PasswordHash to grant access.

Impact:

  • An attacker who intercepts the cookie (network sniffing on HTTP, XSS, browser access) obtains the hash and can replay it to bypass file password protection
  • The cookie lacks HttpOnly (readable by JavaScript), Secure (sent over HTTP), and SameSite flags
  • SHA-1 is a fast hash — short passwords can be brute-forced offline if the server salt is discovered
  • Files sharing the same password have identical hashes (server-wide salt, not per-file), enabling cross-file replay

Evidence:

  • Hash function: configuration/Configuration.go:225-236SHA1(password + SaltFiles)
  • Hash leaked in cookie: Webserver.go:1098
  • Cookie used as auth: Webserver.go:1108 (cookie.Value == file.PasswordHash)
  • Missing cookie flags: Webserver.go:1096-1100
  • OAuth cookie correctly sets HttpOnly: true at oauth/Oauth.go:127 — inconsistency

Fix: Use a random token as the cookie value instead of the hash. Store the mapping server-side. Add HttpOnly, Secure, and SameSite flags.


Bug 3 — LOW: File password comparisons not constant-time

Files: internal/webserver/Webserver.go:584, 1108

// Line 584 — showDownload: if configuration.HashPassword(enteredPassword, true) != file.PasswordHash ... // Line 1108 — isValidPwCookie: if cookie.Value == file.PasswordHash {

Root Cause: Both use Go's ==/!= operator instead of crypto/subtle.ConstantTimeCompare. The codebase already has IsEqualStringConstantTime() at Authentication.go:289-293 used for admin login — inconsistent.

Practical Exploitability: Very low. Line 584 compares hashed values (attacker can't reverse timing to recover the password). The timing difference for 40-char hex strings is nanoseconds, far below network jitter. Artificial delays (1-3 seconds) on failure further obscure signal.

Severity Rationale: Downgraded from HIGH to LOW. Real code defect and best-practice violation, but not practically exploitable over HTTP.

Fix: Replace ==/!= with IsEqualStringConstantTime() for consistency with admin auth.


Bug 4 — HIGH: Session cookie missing HttpOnly flag

File: internal/webserver/authentication/sessionmanager/SessionManager.go:86-93

c := &http.Cookie{ Name: "session_token", Value: sessionString, Expires: expiry, SameSite: http.SameSiteLaxMode, // Missing: HttpOnly: true // Missing: Secure: true }

Root Cause: HttpOnly and Secure are Go bool fields defaulting to false. The session cookie is accessible to JavaScript and sent over plain HTTP.

Impact: Any XSS vulnerability in the application (or future introduction of one) allows immediate session theft via document.cookie. Over HTTP, the session token is transmitted in plaintext.

Evidence:

  • Missing flags: SessionManager.go:86-91
  • OAuth cookie correctly sets HttpOnly: true at Oauth.go:127 — confirms inconsistency
  • Tests at SessionManager_test.go never assert on cookie security attributes

Fix: Add HttpOnly: true and Secure: true to the cookie struct.


Bug 5 — MEDIUM: Token generation skips ApiPermManageFileRequests check

File: internal/webserver/authentication/tokengeneration/TokenGeneration.go:18-40

func Generate(user models.User, permission models.ApiPermission) (string, int64, error) { // Checks: ApiPermReplace, ApiPermManageUsers, ApiPermManageLogs // MISSING: ApiPermManageFileRequests vs UserPermGuestUploads ... }

Root Cause: Generate() validates user permissions for Replace, ManageUsers, and ManageLogs but omits ManageFileRequests. The handler apiURequestSave() at Api.go:1098 also lacks a user-level permission check.

Attack Path:

  1. Authenticated user without UserPermGuestUploads calls POST /auth/token with header permission: PERM_MANAGE_FILE_REQUESTS
  2. Receives a 5-minute temporary API key with ApiPermManageFileRequests
  3. Uses the key to call POST /api/uploadrequest/save — creates file requests they shouldn't be able to create

Evidence:

  • Missing check: TokenGeneration.go:18-27
  • Correct mapping exists in apiModifyApiKey() at Api.go:190-193
  • Intended correspondence confirmed at Api.go:872-873 (UserPermGuestUploadsApiPermManageFileRequests)
  • No test verifies that users without UserPermGuestUploads are blocked from generating file request tokens

Fix: Add to Generate():

if containsApiPermission(permission, models.ApiPermManageFileRequests) && !user.HasPermissionCreateFileRequests() { return "", 0, errors.New("user does not have permission to generate a token with PERM_MANAGE_FILE_REQUESTS") }

Bug 6 — LOW: Content-Disposition filename injection

File: internal/webserver/headers/Headers.go:15-16

w.Header().Set("Content-Disposition", "attachment; filename=""+file.Name+""")

Root Cause: Filenames are stored exactly as uploaded (FileServing.go:300) with no sanitization. A " in the filename breaks the Content-Disposition header structure.

CRLF Injection: NOT exploitable. Go 1.25 (go.mod:3) strips / from header values in net/http.

" Injection: A filename like evil".html produces filename="evil".html", causing browser filename parsing confusion. Low impact — no code execution, but could trick users into saving files with unexpected extensions.

Severity Rationale: Downgraded from MEDIUM to LOW due to Go runtime CRLF protection.

Fix: Sanitize filenames by escaping or stripping " characters before interpolation, or use RFC 5987 filename*=UTF-8''... encoding.


Bug 7 — LOW: OAuth state cookie missing Secure and SameSite

File: internal/webserver/authentication/oauth/Oauth.go:122-130

c := &http.Cookie{ Name: authentication.CookieOauth, Value: value, MaxAge: int(time.Hour.Seconds()), HttpOnly: true, // Missing: Secure, SameSite }

Root Cause: The CSRF-protection state cookie for OAuth lacks Secure (sent over HTTP) and SameSite attributes.

Severity Rationale: Downgraded from MEDIUM to LOW. The state is a one-time 16-char random nonce with a 1-hour expiry, used only during the OAuth callback. The window for practical CSRF exploitation is very narrow.

Fix: Add Secure: true and SameSite: http.SameSiteLaxMode.


Bug 8 — LOW: Wrong permission check in apiUploadRequestListSingle

File: internal/webserver/api/Api.go:1171-1189

func apiUploadRequestListSingle(w http.ResponseWriter, r requestParser, user models.User) { ... if uploadRequest.UserId != user.Id && !user.HasPermission(models.UserPermDeleteOtherUploads) { sendError(w, http.StatusUnauthorized, errorcodes.NoPermission, "No permission to delete this upload request") // Wrong message too

Root Cause: Copy-paste error from apiURequestDelete() at Api.go:1067-1068. Uses UserPermDeleteOtherUploads for a read operation; should use UserPermListOtherUploads (as in apiUploadRequestList() at line 1162).

Impact:

  • Users with UserPermListOtherUploads but not UserPermDeleteOtherUploads are incorrectly blocked from viewing individual upload requests
  • Users with UserPermDeleteOtherUploads but not UserPermListOtherUploads can incorrectly view upload request details

Evidence:

  • Wrong permission: Api.go:1182
  • Correct permission in list-all: Api.go:1162
  • Wrong error message: Api.go:1183 says "delete" for a view operation
  • Copy-paste source: Api.go:1067-1068

Fix: Change UserPermDeleteOtherUploads to UserPermListOtherUploads and fix the error message.

Bug 7 — OAuth error page reflected XSS

File: internal/webserver/Webserver.go:419-428

Reason for Rejection: Go's html/template package auto-escapes all interpolated variables by default. The error template uses standard {{.ErrorProvidedName}} / {{.ErrorProvidedMessage}} syntax — no template.HTML type casts or | safeHTML pipes that would bypass escaping. XSS is prevented by the template engine.


Bug 9 — HTML meta-refresh redirect injection

Files: internal/webserver/Webserver.go:228, internal/webserver/authentication/Authentication.go:297

Reason for Rejection: No user-controlled input reaches the redirect() function. All callers pass either:

  • Hardcoded string literals: "admin", "login", "error", "error?fr", "changePassword", etc.
  • System-generated file.Id values: generated by helper.GenerateRandomString() and cleaned to [a-zA-Z0-9] only by cleanRandomString() at StringGeneration.go:47-51

The vulnerability pattern is real (raw HTML concatenation without escaping) but is a latent risk, not exploitable in the current codebase.

Summary:

This technique shows improvements over the previous one, but not all bug-class specific prompts are equal, when we try “please report all XSS vulnerabilities in this app” the results are much worse, out of 5 reported vulnerabilies, 4 are false-positives and the remaining one is an admin-triggered self-XSS in expired hotlinks, which turned out to be false positive. However, it actually missed a stored XSS vulnerability that we will later discuss when a different tool discovers it.

Claude Code with Skills

Next, what if instead of just prompting more specific, we actually empower Claude with top-tier infosec knowledge? Introducing Claude Skills - specialized, prompt-based instructions and tools that extend the LLM's capabilities. We will explore pairing Claude Code with Trail of Bits's open source security Skills and see what additional value it brings.

We enable a handful of skills, relevant to the target we are auditing:

  • audit-context-building,
  • fp-check,
  • insecure-defaults,
  • sharp-edges,
  • static-analysis,
  • testing-handbook-skills,
  • variant-analysis

then ask for a "comprehensive security audit".

Claude picked up the audit-context-building:audit-context skill and got to work. The results, though, tell a familiar story. The one improvement is that the fp-check skill lets us ask Claude to filter out its own false positives, trimming the report from 21 findings down to 7.

Of those 7, the only new addition is a world-readable config file. Everything else we had already seen before.

Summary:

While the vulnerability discovery process itself did not improve much, the benefit of having a false positive detection skill is noticeable: the report took less time to triage and the findings that made it through are far more actionable.

Claude Code /security-review

We cannot wrap up our Claude testing without trying what is arguably their most established security feature. The /security-review command is designed to review pending changes in the current branch, but with a bit of Git magic we can make the entire repository appear as pending changes. Close enough.

The results are as follows:

The only finding that made it through is the SHA-1 password hashing. No trace of the business logic bugs we uncovered earlier.

Summary:

Quite a disappointing result, especially given that /security-review is probably used a lot by developers who want an accessible AI-assisted security audit without having to think about prompts at all. It did not just fail to find anything new, it actually fell short of what raw Claude Code had already caught with a single generic prompt.

Claude Code Security

We did not manage to get access to Claude Code Security.


OpenAI

Codex

Of course, we also had to put Codex through the same experiment that started this whole journey: a single prompt, asking it to "perform a comprehensive security audit".

Compared to Claude, Codex is noticeably more restrained with its findings, but also more systematic in its approach, starting by mapping out the security boundaries, endpoints, and trust model before diving in.

It came back with 3 findings: 1 high and 2 mediums. The high is the UserPermGuestUploads bypass via an API key with ApiPermManageFileRequests, which we already discussed in the Anthropic section.

The two mediums are ZIP downloads preserving unsafe filenames and allowing archive path traversal (not a vulnerability in Gokapi's context), and the familiar missing security cookie flags.

Full report

1. High: Temporary API token generation allows unauthorized file-request capability

Affected files:

  • internal/webserver/Webserver.go
  • internal/webserver/authentication/tokengeneration/TokenGeneration.go
  • internal/webserver/api/Api.go
  • internal/models/User.go

Relevant code:

  • internal/webserver/Webserver.go:102 exposes /auth/token to any authenticated user.
  • internal/webserver/Webserver.go:299-315 accepts a requested API permission from the request header and forwards it into token generation.
  • internal/webserver/authentication/tokengeneration/TokenGeneration.go:18-39 only checks a subset of elevated permissions before minting a temporary token.
  • internal/webserver/api/Api.go:1098-1156 allows upload request creation and editing once the API permission is present, without checking whether the underlying user is allowed to create file requests.
  • internal/models/User.go:156-158 defines the user-side permission gate for creating file requests.

Impact:

A regular authenticated user can request a temporary API token with PERM_MANAGE_FILE_REQUESTS even if their user account does not have permission to create file requests. That token can then be used against the upload-request API to create public upload endpoints. This is an authorization bypass.

Why this happens:

The temporary token generator validates PERM_REPLACE, PERM_MANAGE_USERS, and PERM_MANAGE_LOGS, but does not validate PERM_MANAGE_FILE_REQUESTS or PERM_API_MOD. The upload-request save path trusts API permission alone and does not re-check the user's HasPermissionCreateFileRequests capability.

Recommendation:

  • Enforce user permission checks for every sensitive API permission in temporary token generation.
  • Add an explicit HasPermissionCreateFileRequests check in the upload-request creation path.
  • Review whether temporary token generation should be available to all authenticated users at all.

2. Medium: ZIP downloads preserve unsafe filenames and allow archive path traversal

Affected file:

  • internal/storage/FileServing.go

Relevant code:

  • internal/storage/FileServing.go:688-695

Impact:

User-controlled filenames are written directly into ZIP entry names. If an attacker uploads files with names such as ../../target or absolute paths, the generated ZIP can contain traversal entries. Extraction behavior depends on the client tool, but this pattern is a known archive exploitation vector.

Why this happens:

The ZIP writer uses file.Name directly after uniqueness handling. There is no normalization to strip path separators, collapse .., or force a basename.

Recommendation:

  • Normalize ZIP entry names before archiving.
  • Strip directory components and reject absolute or traversal-style names.
  • Apply the same normalization when generating the archive filename if user-controlled.

3. Medium: Authentication and password cookies are missing hardening flags, and the file-password cookie stores the password hash directly

Affected files:

  • internal/webserver/authentication/sessionmanager/SessionManager.go
  • internal/webserver/Webserver.go

Relevant code:

  • internal/webserver/authentication/sessionmanager/SessionManager.go:85-92
  • internal/webserver/Webserver.go:1095-1109

Impact:

The session cookie lacks HttpOnly and Secure. The password-protection cookie also lacks hardening flags and stores file.PasswordHash directly as the cookie value. If that cookie is exposed through same-origin script execution, browser extensions, logging, or transport insecurity, it can be replayed to bypass the password gate for that file during the cookie lifetime.

Why this happens:

The application uses the stored password hash itself as the bearer token for future access to password-protected files instead of issuing a separate server-generated proof token.

Recommendation:

  • Set HttpOnly, Secure, and an explicit Path on sensitive cookies.
  • Avoid storing the password hash in the client at all.
  • Replace the password cookie with a random server-side token bound to the file and short expiry.

Notes

  • This review was produced by Codex.
  • Findings are prioritized by likely security impact and exploitability from the code paths reviewed.

Summary:

On the bright side, fewer false positives than Claude, but unfortunately fewer findings overall too. Not quite what you would hope for when asking for a "comprehensive security audit".

Codex Security

While we did not manage to get our hands on Claude Code Security, we did get access to its OpenAI counterpart. Codex Security is OpenAI's application security agent. Fresh out of research preview and already generating buzz, it felt like a natural candidate to explore. Codex Security officially only supports scanning commits from the past two months, but with a few Git tricks we can make it scan the full Gokapi repository.

The result surprised us. Only a single finding: the Content-Disposition header injection we already flagged in the Claude section, where file.Name is potentially attacker-controlled. As we established earlier, CRLF injection is not actually possible here thanks to Go's header validation. So we are looking at one finding, and it has limited impact.

Full report

Severity: Low

Found a security bug: unvalidated filenames flow into HTTP headers, allowing response-splitting/header injection.

The upload pipeline accepts a filename from form data without sanitization, stores it in the file metadata, and later interpolates it directly into the Content-Disposition header. An attacker who can upload (eg, via a public file request or API key) can include CRLF sequences in the filename to inject arbitrary response headers when another user downloads the file, potentially enabling cookie injection or downstream XSS depending on the environment.

Validation:

  • Identify filename intake path and lack of sanitization (Chunking.go:84-110)
  • Verify filename stored in metadata (FileServing.go:297-313)
  • Verify Content-Disposition uses file.Name (Headers.go:13-18)
  • Run PoC with CRLF filename to exercise header writing
  • Check response headers for injected header presence (none observed)

Proof:

internal/storage/chunking/Chunking.go

// ParseFileHeader parses the formdata and returns a FileHeader Filename is taken directly from request form data without sanitization or CRLF filtering. func ParseFileHeader (r *http.Request) (FileHeader, error ) { err := r.ParseForm() if err != nil { return FileHeader{}, err } name := r.Form.Get( "filename" ) if name == "" { return FileHeader{}, errors.New( "empty filename provided" ) } contentType := parseContentType(r) size := r.Form.Get( "filesize" ) if size == "" { return FileHeader{}, errors.New( "empty size provided" ) } sizeInt, err := strconv.ParseInt(size, 10 , 64 ) if sizeInt < 0 { return FileHeader{}, errors.New( "value cannot be negative" ) } if err != nil { return FileHeader{}, err } return FileHeader{ Filename: name, Size: sizeInt, ContentType: contentType, }, nil

internal/storage/FileServing.go:

func createNewMetaData (hash string , fileHeader chunking.FileHeader, userId int , params models.UploadParameters) models.File {

The raw filename is stored in file metadata and preserved for later use in responses.

file := models.File{ Id: createNewId(), Name: fileHeader.Filename, SHA1: hash, Size: helper.ByteCountSI(fileHeader.Size), SizeBytes: fileHeader.Size, ContentType: fileHeader.ContentType, ExpireAt: params.ExpiryTimestamp, UploadDate: time.Now().Unix(), DownloadsRemaining: params.AllowedDownloads, UnlimitedTime: params.UnlimitedTime, UnlimitedDownloads: params.UnlimitedDownload, PasswordHash: configuration.HashPassword(params.Password, true ), UserId: userId, UploadRequestId: params.FileRequestId, } if params.IsEndToEndEncrypted { file.Encryption = models.EncryptionInfo{IsEndToEndEncrypted: true , IsEncrypted: true } file.Size = helper.ByteCountSI(params.RealSize) } if isEncryptionRequested() { file.Encryption.IsEncrypted = true } if aws.IsAvailable() { if !configuration.Get().PicturesAlwaysLocal || !isPictureFile(file.Name) { aws.AddBucketName(&file) internal/webserver/headers/Headers.go internal/webserver/headers/Headers.go func Write (file models.File, w http.ResponseWriter, forceDownload, serveDecrypted bool ) { Content-Disposition header is built by concatenating the unsanitized filename, enabling header injection. if forceDownload { w.Header().Set( "Content-Disposition" , "attachment; filename="" +file.Name+ """ ) } else { w.Header().Set( "Content-Disposition" , "inline; filename="" +file.Name+ """ ) }

Analysis of the appeal

The code path does concatenate unsanitized filenames into Content-Disposition (Chunking.go -> FileServing.go -> Headers.go), but under the standard Go net/http server CR/LF in header values is rejected or sanitized. Without a viable response-splitting exploit, this is not a security issue in practice. The remaining effect is a malformed header value, so the original medium rating is reduced to ignore.

way Upload form filename --ParseFileHeader -> createNewMetaData--> File metadata (Name) --headers.Write sets Content-Disposition--> Content-Disposition header The upload parsing path accepts a filename from form data without sanitization (Chunking.go) and stores it as file metadata (FileServing.go). The download response then concatenates that name into Content-Disposition (Headers.go). This is a classic header-injection pattern, but Go's net/http is expected to reject CR/LF in header values, making response-splitting infeasible in practice. Therefore the issue is likely not a real security vulnerability under the standard Go HTTP stack.

Probability High - Exploitation requires CR/LF to survive into HTTP headers; the standard Go server rejects such values, making successful response-splitting unlikely. | Remote network vector

Impact Ignore - Even though the filename flows into Content-Disposition, Go's HTTP stack is expected to prevent CR/LF header injection, so there is no meaningful security impact beyond a malformed filename. assumptions Go net/http rejects or sanitizes CR/LF in header values, preventing response-splitting even when application code concatenates unsanitized filenames. Attackers can obtain an upload capability (API key or public file request link) in some deployments. Attacker can upload a file (API key or public file request link) A victim downloads the file so Content-Disposition is sent controller Go net/http header value validation blocks CR/LF (assumption)

Weaknesses Did not run a runtime PoC in this environment; relies on Go net/http behavior. If a non-standard HTTP stack or proxy rewrites headers differently, the impact could change. Public upload availability depends on deployment configuration.

We thought the first result was a fluke, so we ran it again. This time things looked very different:

45 findings. That is a lot to unpack. Let's see how many of them actually hold up.

The first high targets the OpenID Connect SSO integration. Gokapi lets you restrict access to specific OIDC "Authorised groups", with wildcard support:

The function that checks a user's group against the configured allowlist looks like this:

func matchesWithWildcard(pattern, input string) (bool, error) { components := strings.Split(pattern, "*") if len(components) == 1 { // if len is 1, there are no *'s, return exact match pattern return regexp.MatchString("^"+pattern+"
quot;
, input) } var result strings.Builder for i, literal := range components { // Replace * with .* if i > 0 { result.WriteString(".*") } // Quote any regular expression meta characters in the // literal text. result.WriteString(regexp.QuoteMeta(literal)) } return regexp.MatchString("^"+result.String()+"
quot;
, input) }

Noticed the bug? The default branch (when wildcards are present) correctly calls regexp.QuoteMeta on the input, but the early exit branch (no wildcards) skips it entirely. This means if the configured allowed group is john.doe, someone with the group johnXdoe will sail right through the allowlist. A genuine vulnerability, though we would call it a medium in practice given that an attacker would need some degree of control over SSO identities to exploit it (the Gokapi's maintainer reached a similar conclusion and decided not to issue a CVE for it).

The second high impact vulnerability, "External StreamSaver MITM", flags a situation where Gokapi is running over plain HTTP. Since service workers cannot be registered over insecure connections, the app falls back to a proxy hosted on the creator's GitHub Pages. The man-in-the-middle potential is real, but this looks like a deliberate tradeoff: the service worker exists specifically to avoid buffering large encrypted files in memory (and frankly, if you are running a production file service over HTTP you have bigger problems - why don't just MITM the connection itself?).

The other two highs are unauthenticated setup and header-based auth, both of which we have already seen and both of which are intentional behaviours.

Moving on to the 8 mediums, we were able to confirm 2 as valid, and they both hit the same feature: hotlinking. You cannot create hotlinks for HTML pages, but what if you create a hotlink for a PNG and then swap it out for an HTML file? The check gets bypassed. The same goes for SVGs. Both result in a stored XSS reachable by any authenticated user. Nice finding.

The remaining mediums fall into the "weakness or false positive" bucket given Gokapi's threat model.

Most of the lows follow the same pattern, with a few exceptions worth calling out: the "Cleanup deletes all files" bug we already know well by now, the SVG hotlink XSS surfacing again, and a log injection vector via username.

So out of 45 findings, only a handful have real, direct impact. Notably, the business logic bug around UserPermGuestUploads vs ApiPermManageFileRequests, which Codex itself found in the previous run, is nowhere to be seen this time around. Even the rest of the findings are worth knowing about and fixing, but they do not pose immediate risk.

Full report

Codex Security Findings

Total findings: 43 (4 High, 8 Medium, 31 Low)


High Severity

1. OIDC user/group restriction bypass via regex meta characters

Severity: High

Description: The commit replaces exact string comparisons for allowed OIDC users/groups with a regex-based wildcard matcher. When no wildcard is present, the code builds a regex from the raw pattern without escaping meta characters (e.g., '.', '+', '?'). Since typical usernames/emails include '.', the check becomes a regex match instead of a literal match. An attacker with a crafted OIDC username or group value can satisfy the regex and bypass intended restrictions to reach the admin UI. Escaping the pattern for the no-wildcard branch (e.g., regexp.QuoteMeta) is required to restore literal matching semantics.


2. Encrypted downloads routed through external StreamSaver MITM

Severity: High

Description: In the encrypted-download path, the download template sets streamSaver.mitm to https://bulling-it.de/gokapi/serviceworker.html. StreamSaver uses this MITM page/service worker to proxy the decrypted stream. Because the service worker is hosted on an external origin, that origin can access the plaintext file data (or be swapped/compromised to exfiltrate it). This breaks the expected self-hosted trust boundary and leaks file contents whenever client-side decryption is used (e.g., encrypted S3 downloads).


3. Unauthenticated setup webserver exposed on first start

Severity: High

Description: RunIfFirstStart now launches the setup webserver before any configuration is loaded. During initial setup, the basicAuth wrapper explicitly skips authentication, and the setup server listens on ":<port>" (all interfaces). This means any network client that can reach the service during first start can access /setup and POST /setup/setupResult to define admin credentials, authentication method, and storage settings, effectively taking over the instance before the legitimate operator completes setup.


4. Header-based auth trusts client headers, enabling auth bypass

Severity: High

Description: The commit adds header-based authentication and treats any request with a configured header as authenticated. There is no validation that the request came from a trusted reverse proxy, no IP allowlist, and (when LoginHeaderForceUsername is false) no username verification. As a result, if LOGIN_HEADER_KEY is set and the service is reachable directly, an attacker can add the header with any value (or the admin name when forced) and bypass login to access admin endpoints, create/delete API keys, and manage uploads.


Medium Severity

5. Unauthenticated setup page now exposes S3 credentials

Severity: Medium

Description: The change makes the initial setup view load cloudconfig even when no main configuration exists. The setup template now renders the S3 bucket, key ID, secret, and endpoint when S3 env vars are not provided. Because the setup UI is explicitly unauthenticated during initial setup, anyone who can reach /setup/start during first-run can view these credentials if a cloudconfig.yml is pre-provisioned (or left over). Previously, the cloud settings block was only rendered during authenticated reconfiguration, so the secrets were not exposed in the unauthenticated initial setup flow.


6. Presigned URL cache never cleaned, enabling memory DoS

Severity: Medium

Description: The commit replaces database-backed presigned URLs with an in-memory map and adds a cleanup goroutine. However, cleanUp() sets cleanupStarted to true and never resets it, so subsequent cleanup calls (including the scheduled periodic cleanup) immediately return. Because Get() does not delete expired entries, any presigned URL that is created but never downloaded remains in memory forever. An authenticated user with download permission can repeatedly request presigned ZIP URLs without using them, causing unbounded memory growth and a potential denial-of-service.


7. File replacement keeps hotlink IDs, enabling inline XSS via hotlink

Severity: Medium

Description: The commit adds storage.ReplaceFile, which swaps file content metadata (name, content type, encryption, SHA1, etc.) but never revalidates or clears the existing HotlinkId. Hotlinks are intended only for images that are not encrypted or password-protected (IsAbleHotlink), yet showHotlink serves any file referenced by a hotlink inline without re-checking eligibility. This lets an attacker with REPLACE permission upload or reference a non-image (e.g., HTML/JS), replace a hotlinkable image, and then deliver that content inline from the Gokapi origin via the preserved hotlink URL, enabling stored XSS and session theft/CSRF against users who open the link.


8. Custom SSE broadcasting can exhaust goroutines via slow clients

Severity: Medium

Description: The custom SSE server maintains a listener map and publishes each status update by spawning a goroutine that sends into an unbuffered per-connection channel. The handler writes replies synchronously to the client; if a client stops reading (or is very slow), the handler blocks on WriteString and can no longer drain the reply channel. Subsequent PublishNewStatus calls will spawn goroutines that block forever on channel sends. An authenticated attacker can open a SSE connection and stall reads while triggering many status updates (e.g., uploading many small files), causing unbounded goroutine and memory growth and degrading or crashing the service.


9. Unsynchronized download status map allows remote DoS

Severity: Medium

Description: Download status tracking was moved into an in-memory map (internal/webserver/downloadstatus). The new map is accessed by HTTP handlers (SetDownload/SetComplete) and by the periodic cleanup routine without any mutex. Go maps are not safe for concurrent read/write, so multiple simultaneous download requests (or downloads overlapping with cleanup) can trigger a fatal "concurrent map writes" panic and crash the server. This enables a remote, unauthenticated denial-of-service by simply requesting multiple downloads concurrently.


10. AWS presigned downloads now render user-supplied content inline

Severity: Medium

Description: The commit changes AWS presigned download generation to set an inline Content-Disposition and to pass the upload-provided Content-Type back to S3. Since file.ContentType comes directly from the multipart upload headers without validation, an attacker can upload HTML/SVG content and have it rendered inline when a hotlink is accessed. This enables script execution on the S3 endpoint; if the endpoint shares the host with Gokapi (common with self-hosted S3/MinIO), the script can read non-HttpOnly cookies and potentially hijack sessions.


11. Unpinned GitHub Action in release pipeline with Docker secrets

Severity: Medium

Description: The new release workflow adds the oprypin/find-latest-tag@v1 action. This action is referenced by a mutable tag instead of a commit SHA. Because the same job later logs into Docker Hub using secrets and builds/pushes images, a compromised or retagged action could execute arbitrary code in the CI runner, exfiltrate credentials, or tamper with the image build process.


12. CI workflow uses unpinned GitHub Actions and latest buildx

Severity: Medium

Description: The added GitHub Actions workflow pulls third-party actions using mutable tags (actions/checkout@v2, crazy-max/ghaction-docker-buildx@v1) and explicitly installs version: latest for buildx. These references are not pinned to immutable commits or version hashes. If the upstream action repository is compromised or the tag is retargeted, the attacker can execute arbitrary code during CI runs and exfiltrate Docker Hub secrets used later in the workflow.


Low Severity

13. Docker subnet auto-trust enables proxy IP spoofing

Severity: Low

Description: The commit introduces automatic addition of a detected Docker/private subnet to the trusted proxy list when running a Docker build. Any request originating from that subnet is treated as a trusted proxy, so the server will accept X-Forwarded-For and X-Real-IP values as the client IP. If an attacker can connect from another container or host on the same subnet (or when using host networking), they can supply arbitrary XFF values, bypassing IP-based rate limiting and corrupting audit logs.


14. Unvalidated CF-Connecting-IP header allows IP spoofing

Severity: Low

Description: The new Cloudflare support returns the CF-Connecting-IP header as the requester IP whenever GOKAPI_USE_CLOUDFLARE is enabled, without checking that the immediate connection actually comes from a Cloudflare IP range or a trusted proxy. If the origin server is reachable directly (a common misconfiguration), an attacker can send arbitrary CF-Connecting-IP values to spoof their IP. This undermines rate-limiting and audit logging that depend on GetIpAddress, enabling brute-force attempts and log obfuscation.


15. API key usage cache indexes wrong cache bucket

Severity: Low

Description: dbcache.RequireSaveApiKeyUsage was introduced to reduce database writes, but it uses cacheStore[TypeUserLastOnline] instead of cacheStore[TypeApiLastUsed]. This mixes API key usage tracking into the user-online cache entry, defeating the intended separation and creating unnecessary contention and future maintenance risks (e.g., any changes to the user-online cache could inadvertently affect API key usage throttling).


16. Cleanup deletes all files when orphaned file request exists

Severity: Low

Description: The commit introduces cleanInvalidFileRequests to purge file requests whose owner has been removed. Inside the loop, DeleteFile is called unconditionally for every file because the UploadRequestId check body is empty. As soon as any orphaned file request exists (e.g., after an admin deletes a user), the cleanup routine will delete all stored files rather than only those tied to that request. This is an availability/integrity issue that can be triggered by any actor able to create a file request and then cause the owning user to be removed (e.g., via admin user management) or by DB corruption.


17. CLI upload-dir panics with --json due to nil progress bar

Severity: Low

Description: In cmd/cli-uploader/Main.go, zipFolder only initializes progressBar when showOutput is true. When JSON output is requested (--json), showOutput is false, leaving progressBar nil. The code later calls progressBar.Describe unconditionally for each file, causing a nil-pointer panic and aborting the upload. This is a reliability issue introduced by the commit and affects the CLI upload-dir feature.


18. crypto/rand errors ignored in security token generation

Severity: Low

Description: The commit changes generateRandomBytes to ignore errors from crypto/rand.Read and removes the fallback that at least warned operators. GenerateRandomString is used for session IDs, API keys, OAuth state, and similar secrets. If the OS CSPRNG fails or returns a short read, the buffer remains zeroed or partially filled and the code silently encodes it, producing predictable tokens. This could allow guessing session IDs or API keys under RNG failure conditions.


19. Base64-decoded filenames can inject control bytes into headers

Severity: Low

Description: In the API chunk completion path, filenames prefixed with "base64:" are decoded and assigned directly to FileHeader.Filename without filtering for control characters or invalid bytes. Because the stored file name is later used verbatim when constructing Content-Disposition headers, an attacker with upload permission can base64-encode NUL/CTL bytes that would have been rejected by the HTTP header parser. These bytes can corrupt response headers or logs in downstream systems and may enable header/log injection in some proxies or clients.


20. Password reset no longer invalidates existing sessions

Severity: Low

Description: The deployment password reset path now updates the super-admin password but no longer deletes active sessions. If an attacker has already stolen or fixed a valid session token, they will retain authenticated access even after an operator resets the password, defeating an important recovery step. The previous implementation cleared sessions on reset; the new logic omits this invalidation.


21. GC rebuild can drop concurrent upload status updates

Severity: Low

Description: The new GC logic takes a snapshot via GetAll() and only afterwards acquires the write lock and replaces statusMap. Any Set() call that occurs between the snapshot and the lock writes to the old map, but that entry is lost when statusMap is replaced. This can cause recently updated upload statuses to disappear from the in-memory store, leading to missing or stale status updates for SSE clients. It is a correctness/availability bug rather than a security flaw.


22. Recursive upload-status GC can grow stack indefinitely

Severity: Low

Description: The commit introduces an in-memory UploadStatus store with a background garbage collector. The GC function uses recursion with a periodic time.After and never returns, so each hour adds another stack frame. Over long uptimes this leaks stack memory and can lead to process termination, impacting availability. Since uploads trigger the GC, a low-privileged uploader can start the leak and wait for eventual exhaustion.


23. Upload password stored in localStorage exposes defaults

Severity: Low

Description: The commit moved upload default persistence from server-side storage to the browser. The admin UI now stores the upload password in localStorage and restores it on each admin page load. localStorage is accessible to any JavaScript running on the same origin (including unauthenticated pages), so a stored XSS or malicious script could extract the default password and use it to access password-protected uploads.


24. SQLite API key upgrade can panic on legacy keys

Severity: Low

Description: The SQLite migration adds Expiry and IsSystemKey columns without backfilling existing rows. Legacy API keys therefore have NULL values in these new columns. The updated GetApiKey scans Expiry/IsSystemKey into int64/int and calls helper.Check on any scan error. When a legacy API key is used (common after upgrade), the NULL scan triggers a panic, crashing the server. This creates an availability issue: anyone who can present a pre-upgrade API key can remotely crash the service until restart.


25. Redis prefix injection allows arbitrary Lua in session cleanup

Severity: Low

Description: The new Redis backend accepts a user-supplied key prefix (from setup or config URL). That prefix is stored in the database config and later concatenated directly into a Lua script passed to Redis EVAL in deleteAllWithPrefix. Because the prefix is not escaped or passed as an argument, a crafted value containing quotes can break out of the string and execute arbitrary Redis commands (e.g., FLUSHALL). The cleanup path is invoked when all sessions are deleted (e.g., after setup/reconfigure), so an attacker able to set the Redis prefix during setup can trigger Redis command injection affecting integrity/availability of the Redis database.


26. Potential deadlock in SSE shutdown due to lock held on send

Severity: Low

Description: The updated Shutdown() holds the RWMutex read lock while iterating listeners and calling channel.Shutdown(), which performs a blocking send on an unbuffered shutdownChannel. If a client disconnects at the same time (ctx.Done case), GetStatusSSE() calls removeListener(), which needs the write lock. With the read lock held, removeListener blocks, the goroutine stops receiving from shutdownChannel, and Shutdown() blocks on the send, causing a deadlock and preventing graceful shutdown.


27. S3 upload path now ignores storage errors, risking silent data loss

Severity: Low

Description: NewFile now calls FileExists before S3 uploads. FileExists logs S3 HeadObject errors and returns true instead of failing. As a result, transient S3 errors, permission issues, or timeouts can cause the upload to be skipped while metadata is still saved, leaving the system with broken download links and data loss. This is an integrity/availability bug introduced by the refactor rather than a direct security vulnerability.


28. Docker entrypoint now runs as root by default

Severity: Low

Description: The updated dockerentry.sh only switches to the non-root gokapi user when the DOCKER_NONROOT environment variable is provided. Otherwise it executes /app/gokapi directly as root. This is a security regression because the container no longer defaults to least privilege; any application compromise (e.g., via a separate bug) yields root in the container, increasing impact, especially when host volumes are mounted or privileged container settings are used.


29. Unsanitized SUDO_USER allows systemd unit file injection

Severity: Low

Description: The new getUserInvokingSudo() function pulls the username from the SUDO_USER environment variable and passes it unvalidated into createSystemdFileContent(), which concatenates it into the systemd unit file. Because environment variables can contain newlines, a user who can run the installer with sudo (including restricted sudoers entries) can set SUDO_USER to include newline-separated directives (e.g., resetting ExecStart and adding a new ExecStart command), resulting in arbitrary command execution or a service running as root. The previous code used user.Current().Username, which comes from /etc/passwd and is not attacker-controlled, so this injection path is introduced by the commit.


30. OIDC group claim parsing can panic on unexpected types

Severity: Low

Description: The commit adds group-based OIDC restrictions by parsing the configured claim from the userinfo response. The parsing code uses unchecked type assertions on the claim value and its elements. If the identity provider returns the group claim as a string (or any non-array type), or includes non-string elements, the handler panics. Panics in an HTTP handler abort the request and can be repeatedly triggered to deny OAuth logins. Because the claim originates from an external IdP, this is untrusted input and should be validated before type assertions.


31. Unescaped PublicName injected into expired SVG hotlink response

Severity: Low

Description: The new expired image is generated by parsing expired_file_svg.tmpl with text/template, which does not provide context-aware escaping. The template embeds {{.PublicName}} directly into the SVG. PublicName is populated from the setup form and stored in configuration without validation. The resulting SVG is served with image/svg+xml for expired hotlinks. If an attacker can influence PublicName (e.g., during initial setup or via config editing), they can inject arbitrary SVG markup or script, leading to stored XSS when a victim views the expired hotlink.


32. Nil pointer panic in chunk upload causes server crash

Severity: Low

Description: In internal/storage/chunking/Chunking.go, the new defer file.Close() statements are placed immediately after os.OpenFile/GetFileByChunkId and before checking err. When these calls fail (e.g., a very long user-supplied chunk UUID causes ENAMETOOLONG, or filesystem errors occur), file is nil and the deferred Close triggers a nil-pointer panic on return. Because chunk UUIDs come from user requests and are not length-limited, a malicious upload can reliably trigger this path, resulting in a process crash (DoS).


33. E2E WASM AddFile can deadlock on missing upload ID

Severity: Low

Description: In cmd/wasme2e/Main.go, AddFile acquires fileMutex and immediately returns an error if the upload ID is not found. The early return skips the mutex unlock, leaving fileMutex locked permanently. Any later AddFile call will block forever, effectively breaking the end-to-end upload flow after a single invalid call.


34. Nil dereference in AWS FileExists error handling

Severity: Low

Description: The new timeout logic in FileExists performs aerr.Code() outside the ok check from the type assertion. If err is not an awserr.Error (e.g., URL parsing or other non-AWS errors), aerr is nil and calling Code() panics. FileExists is used during AWS login validation and file operations, so a crafted or incidental non-AWS error can crash the request path or startup flow, resulting in a denial-of-service condition.


35. Chunk completion accepts path traversal via unsanitized chunkid

Severity: Low

Description: The chunked upload flow introduces /uploadComplete and /api/chunk/complete. In fileupload.CompleteChunk, the chunkid is taken directly from the request without any sanitization. storage.NewFileFromChunk then builds a filesystem path using DataDir + "/chunk-" + chunkId and opens it with read/write access. Because the chunk ID is not restricted to safe characters, an authenticated attacker can supply values such as ../../../../etc/passwd (with enough .. segments) to traverse out of the data directory. This lets them read, rename/move, or delete arbitrary files writable by the server process, and the data can be exposed as a downloadable file via Gokapi metadata.


36. AWS dedup skips upload but keeps new encryption key

Severity: Low

Description: NewFile now checks S3 for an existing object and, when found, skips uploading the newly encrypted data. However, the function still stores the freshly generated EncryptionInfo from the new upload without reconciling it with the existing object. For full-encryption modes, each upload uses a new random key; skipping the upload means the S3 object remains encrypted with the old key while metadata points to the new key. Clients attempting to decrypt the file will fail, effectively corrupting duplicate uploads on AWS-backed storage.


37. Unauthenticated setup AWS test enables SSRF via custom endpoint

Severity: Low

Description: The commit adds a /setup/testaws handler that builds an AwsConfig from user-controlled JSON and calls aws.IsValidLogin and aws.IsCorsCorrectlySet. During initial setup, basicAuth is bypassed, so this endpoint is unauthenticated. The AWS client session uses awsConfig.Endpoint directly with no validation, so a remote user can force the server to connect to an arbitrary host/port and issue signed HTTP requests, enabling SSRF-style network access (e.g., internal service probing) while the setup server is exposed.


38. No-AWS setup fails due to missing storage_sel_image field

Severity: Low

Description: The commit adds parsing for "storage_sel_image" in parseServerSettings. However, the setup template only includes the corresponding <select> when .HasAwsFeature is true. In builds compiled without AWS support, the field is omitted from the form submission, causing getFormValueString to return an error and the setup to fail. This is a regression that prevents initial configuration and reconfiguration on no-AWS builds.


39. Logging uses env DataDir and can crash if config differs

Severity: Low

Description: Load() creates the directory from the configuration (serverSettings.DataDir) but now initializes logging with Environment.DataDir. If an operator changes DataDir in config.json without also setting GOKAPI_DATA_DIR, the log path points to a directory that may not exist. Because logging uses helper.Check on os.OpenFile and write errors, any download (or other logging event) can panic and crash the process. This is a regression introduced by the commit and can be triggered by normal requests once misconfigured.


40. Unsanitized download logging enables log entry injection

Severity: Low

Description: The commit introduces a logging package and calls it on every download. The log entry is built from file.Name (sourced from the upload filename) and r.UserAgent() with no stripping of control characters before being appended to log.txt. An attacker who can upload a file or control the User-Agent header for a download can inject newlines or crafted content into the log, forging entries or obscuring audit trails. Sanitizing/escaping control characters before logging, or structured logging, would prevent log injection.


41. Hotlink endpoint serves SVG inline, enabling stored XSS

Severity: Low

Description: The commit adds hotlinks for image files and includes .svg as a supported extension. The /hotlink/ handler serves these files directly via ServeFile with forceDownload=false, so no Content-Disposition is set and the response is rendered inline. Because SVG can contain script, an attacker who can upload a file (e.g., via a public upload or low-privileged account) can upload a malicious SVG and share the hotlink. When a victim opens or embeds the hotlink, the script executes in the Gokapi origin, enabling session theft or admin actions. This was not possible before because downloads were forced as attachments.


42. File password hashes are reused as auth cookies

Severity: Low

Description: The new password-protection feature hashes file passwords with a fixed salt using SHA1 and stores the hash in metadata. When a user enters the correct password, the server sets a cookie whose value is the stored hash, and subsequent downloads only verify that cookie value. This makes the hash a bearer token: anyone who can obtain it (e.g., from config.json, upload responses, or a stolen cookie) can bypass the password without knowing the plaintext. Because SHA1 is fast and the salt is static, the hash is also vulnerable to offline guessing if exfiltrated.


43. Docker default binds to 0.0.0.0, exposing admin UI

Severity: Low

Description: The new configuration flow forces Docker runs to skip the local-only binding prompt. Because the Dockerfile creates a .isdocker marker and askForLocalOnly() returns false in that case, generateDefaultConfig() sets the listen address to 0.0.0.0. If operators publish the container port without additional network restrictions, the admin interface and upload/download endpoints become reachable from external networks by default. This expands the attack surface and enables remote password-guessing attempts on the admin login.

Our classification of them:

#FindingSeverityOur Classification
5Setup exposes S3 credsMediumAccepted risk
6Presign cache memory DoSMediumWeakness
7Hotlink XSS via replaceMediumVulnerability
8SSE goroutine exhaustionMediumFalse Positive
9Download status map DoSMediumWeakness?
10AWS inline content XSSMediumVulnerability
11Unpinned GH Action releaseMediumWeakness
12Unpinned GH Actions CIMediumWeakness
13Docker subnet IP spoofingLowAccepted risk
14CF-Connecting-IP spoofingLowAccepted risk
15Cache bucket mismatchLowWeakness
16Cleanup deletes all filesLowVulnerability
17CLI nil progress barLowWeakness
18crypto/rand errors ignoredLowWeakness
19Base64 filename injectionLowWeakness
20Password reset sessionsLowWeakness
21GC race drops statusLowWeakness
22Recursive GC stack leakLowWeakness
23localStorage passwordLowAccepted risk
24SQLite legacy key panicLowFalse Positive
25Redis Lua injectionLowWeakness
26SSE shutdown deadlockLowWeakness
27S3 upload fail-openLowFalse Positive (not security)
28Docker root defaultLowAccepted risk
29SUDO_USER injectionLowWeakness
30OIDC group panicLowWeakness?
31SVG PublicName XSSLowWeakness
32Chunk nil panicLowFalse Positive
33WASM AddFile deadlockLowWeakness
34AWS FileExists nil derefLowFalse Positive
35Chunk path traversalLowFalse Positive
36AWS dedup encryption keyLowFalse Positive
37Setup SSRF via AWS testLowAccepted risk
38No-AWS setup missing fieldLowWeakness
39Logging DataDir crashLowWeakness
40Log injectionLowVulnerability
41SVG hotlink XSSLowVulnerability
42Password hash cookiesLowAccepted risk
43Docker 0.0.0.0 bindingLowAccepted risk

Summary:

The signal to noise ratio is rough, but the signal itself is worth noting. The stored XSS that every other tool had missed up to this point is a genuinely good find, and the SSO bug is a nice catch too. Less impressive is the tendency to report the same vulnerability multiple times from different angles rather than consolidating findings around a root cause, which makes the report harder to navigate than it needs to be.

It also has to be said that Codex Security is still clearly early: the UI has quite a few rough UI/UX bugs that make the experience more painful than it should be.


AISafe Labs

Thus far we have explored individual LLMs and how well they perform when given increasingly richer context and more detailed instructions. From a single generic prompt, to bug-class specific ones, to a full suite of infosec skills bolted on top. Now we have reached the final boss, how much extra value will the custom orchestration, knowledge base and tools built into AISafe bring?

Preview of AISafe app

The audit returned 2 high impact vulnerabilities.

The first one is titled "Data Leak in Upload Status Stream". Gokapi uses HTTP server-sent events to stream upload progress to the user's browser. The problem is that authorization on this endpoint is completely broken: any authenticated user receives events about every file upload happening across the entire application. That includes chunk IDs, which could allow an attacker to interfere with another user's active upload, and file IDs, which would let them download files they were never meant to see. This is a serious finding, and it is genuinely surprising that none of the other tools in this experiment spotted it.

The second high is the unauthenticated setup wizard, which as we have established is an accepted risk for Gokapi.

14 mediums. Let's go through them, because there is a mix of familiar faces and some interesting new ones.

Two we have already seen: the UserPermGuestUploads vs ApiPermManageFileRequests privilege escalation, and the stored XSS in hotlinks.

The first novel finding is a nice catch around API key demotion. When a user gets demoted, previously issued API keys do not automatically lose the privileges they should. In practice, a stale key can retain ApiPermManageFileRequests and keep managing upload requests long after the account behind it no longer has that capability.

The second is a privilege escalation in the file replacement feature. Cross-user replacement is guarded too loosely: if you have permission to list other users' uploads, you can point idNewContent at someone else's file during a replacement and, with deleteNewFile=true, have the storage layer silently delete it without any stronger authorization check. The victim's file is gone.

The third new finding targets chunk uploads. The size of the current HTTP request body is checked, but the total declared file size is taken from attacker-controlled metadata and then used during allocation and completion. By splitting a file into chunks and lying about the right fields in the metadata, an attacker can smuggle a file that exceeds the configured upload limit. A clean quota bypass.

That is 2 familiar findings and 3 new ones. What about the remaining 9?

Two are CSRF reports, one for the login page and one for the password reset page, but this time with actual impact attached. AISafe noticed, that the password reset functionality uses r.Form.Get("newpw"), which accepts both POST and GET parameters with no method check, effectively bypassing the SameSite=Lax default protection that would otherwise limit CSRF exposure.

Two more are denial of service findings: one in the E2E Metadata Parser where the content parameter is stored in memory twice, creating an amplification factor; and one in the Upload Status SSE Endpoint, which we already know but have determined has no meaningful real-world impact.

The remaining five are a mixed bag:

  • SSRF in the setup page: accepted risk
  • Authorization "bypass" via file add API: determined by the maintainer to have no security implications
  • Race condition in chunk file upload: theoretical due to a very narrow race window
  • Header spoofing in header auth mode: already discussed, intentional behaviour
  • Sessions not invalidated after password change: a valid weakness worth fixing

The remaining 4 lows are mostly theoretical or intentional behaviour.

Full report

(only a shorten version of the report)

Vulnerability Findings

AIS-MSO-GKP-001 - Privilege Escalation in Token Minting

Severity: Medium Code: AIS-MSO-GKP-001

Description: The token minting flow allows an authenticated user to request an API permission scope that is not tied to the user's effective capabilities. A caller-controlled permission header is parsed and used to generate an API key, but the validation path omits a check for ApiPermManageFileRequests. This creates a path where a user without Guest Upload privileges can still mint a token that authorizes file-request management. The over-privileged key is then accepted by the API dispatcher for upload-request routes that require ApiPermManageFileRequests, and the request creation sink executes without a user-level Guest Upload authorization check.

Impact: An authenticated user who should not be allowed to create public upload requests can still generate links and API keys for external uploads. This bypasses tenant policy controls, expands data-ingress exposure, and can be abused to introduce unapproved content or consume storage and operational capacity.


AIS-MSO-GKP-002 - Auth Bypass in Initial Setup

Severity: High Code: AIS-MSO-GKP-002

Description: During first-run state, setup endpoints can be reached without prior authentication and accept attacker-controlled configuration input. The setup middleware explicitly forwards requests when initial setup is active, and the setup result handler is exposed over HTTP. The submitted setup payload is transformed into runtime authentication and configuration values and then applied to the instance. The flow reaches persistent configuration and super-admin credential update sinks, allowing remote takeover of a fresh deployment.

Impact: If the service is internet-reachable before trusted administrators complete setup, an external attacker can become the controlling administrator, persist malicious settings, and gain ongoing access to sensitive files and operational functions.


AIS-MSO-GKP-003 - Header Spoofing in Trusted Auth

Severity: Medium Code: AIS-MSO-GKP-003

Description: In trusted-header mode, identity is derived directly from a caller-supplied HTTP header without attesting that the request originated from a trusted proxy. The authentication flow reads the configured header, maps it to a local user, and treats the request as authenticated. This identity is then used to access privileged routes such as the admin interface. Setup values also define which header-driven identity is considered administrative, so a direct client that can reach the backend can spoof privileged usernames.

Impact: When the backend is directly reachable, an attacker can bypass upstream SSO assumptions by forging trusted headers and impersonating other users, including administrative identities. This can result in unauthorized administration and data compromise.


AIS-MSO-GKP-005 - Session Persistence After Password Change

Severity: Medium Code: AIS-MSO-GKP-005

Description: The password change flow updates the user password record but does not revoke existing sessions for that account. Session authentication continues to trust previously issued session_token cookies backed by session store entries.

Impact: An attacker with a previously captured session can retain account access even after the legitimate user changes their password, undermining incident recovery and prolonging unauthorized activity.


AIS-MSO-GKP-006 - CSRF in Password Reset Handler

Severity: Medium Code: AIS-MSO-GKP-006

Description: The password change endpoint accepts state-changing input from parsed form parameters without CSRF validation and without strict method enforcement. The route allows requests to reach the handler, then reads newpw directly from request data and commits the new password.

Impact: A victim in an authenticated context can be forced to submit an attacker-chosen password update, which can lead to account takeover and disruption of legitimate access.


AIS-MSO-GKP-007 - Race Condition in Presigned Download

Severity: Low Code: AIS-MSO-GKP-007

Description: The one-time presigned download path uses a non-atomic read-then-delete sequence for token consumption. A user-controlled key parameter is looked up first, and invalidation occurs later, leaving a replay window for concurrent requests. The backing token store exposes separate Get and Delete operations rather than an atomic consume primitive, which allows multiple in-flight requests to pass validation before deletion takes effect.

Impact: A leaked or intercepted presigned URL can be replayed in parallel to retrieve protected content multiple times, increasing the scope of unauthorized disclosure beyond intended single-use semantics.


AIS-MSO-GKP-008 - Quota Bypass in Chunk Upload Size

Severity: Medium Code: AIS-MSO-GKP-008

Description: Chunk upload handling enforces request-size checks on the current HTTP body but trusts attacker-controlled total file size metadata for allocation and completion. The flow reads filesize or dztotalfilesize, stores it, and truncates storage based on that untrusted value. At API entry, the limit check uses request content length and does not enforce the upload-request-specific max on the declared total payload. Final file-size validation also references global policy rather than request-level quota boundaries.

Impact: Recipients of constrained upload requests can submit files larger than intended limits, which weakens governance controls and can drive excess storage usage and operational cost.


AIS-MSO-GKP-009 - Race Condition in File Quota Checks

Severity: Medium Code: AIS-MSO-GKP-009

Description: File-request MaxFiles enforcement uses check-then-act logic that is not synchronized with reservation and completion state updates. Concurrent requests can pass remaining-file checks based on stale counters and then reserve or complete additional uploads. A similar timing window exists in completion flow, where admission checks occur before metadata persistence that effectively increments file count, enabling over-commit under parallel execution.

Impact: Attackers using a shared upload request can exceed configured file-count limits, resulting in quota bypass, storage abuse, and reduced reliability of upload governance controls.


AIS-MSO-GKP-010 - Secret Exposure in Config File

Severity: Low Code: AIS-MSO-GKP-010

Description: Configuration data containing sensitive authentication and encryption fields is written to disk with permissive file mode settings. The write path creates or truncates the config file using world-readable permissions, while serializing the full settings object. The persisted models include sensitive values such as authentication secrets and encryption material, so local users with read access to the host filesystem may obtain credentials or cryptographic data.

Impact: A local attacker or co-tenant process that can read the configuration path can extract secrets that support impersonation, data decryption, or further lateral movement within the environment.


AIS-MSO-GKP-012 - SSRF in Setup AWS Test Endpoint

Severity: Medium Code: AIS-MSO-GKP-012

Description: The setup AWS test handler accepts attacker-controlled endpoint values and uses them to configure outbound AWS SDK calls. During initial setup conditions, authentication checks are bypassed for setup routes, allowing untrusted users to reach this network-capable functionality. The endpoint value is injected into AWS client configuration and then used by HeadObjectWithContext and GetBucketCorsWithContext, creating attacker-influenced outbound requests from the server process.

Impact: An unauthenticated attacker can induce server-side network requests to internal or external destinations while setup is exposed, which can aid internal service probing and broaden attack reach.


AIS-MSO-GKP-013 - Data Leak in Upload Status Stream

Severity: High Code: AIS-MSO-GKP-013

Description: The upload status SSE implementation publishes global upload state to any authenticated listener and includes file_id values that are not scoped to the requesting user. Status records are stored in a shared map and replayed in bulk when listeners connect. The leaked identifiers can then be used against the file download path, where id directly selects content to serve.

Impact: Any authenticated user can observe other users' file identifiers and retrieve unauthorized content, causing cross-tenant data exposure and loss of confidentiality for uploaded documents.


AIS-MSO-GKP-014 - DoS in Upload Status SSE Endpoint

Severity: Medium Code: AIS-MSO-GKP-014

Description: The upload status SSE handler creates listener entries for each authenticated connection without enforcing connection caps. Listener instances are retained for long durations and broadcast fan-out creates goroutines per listener, which increases memory and scheduler pressure under abuse. Because /uploadStatus is accessible to authenticated users, one account can establish many long-lived streams and degrade service capacity.

Impact: An authenticated attacker can consume connection slots and server resources at scale, reducing availability of status streaming and potentially impacting overall application responsiveness.


AIS-MSO-GKP-015 - DoS in E2E Metadata Parser

Severity: Medium Code: AIS-MSO-GKP-015

Description: The E2E metadata endpoint parses attacker-controlled request content through multiple memory-intensive decoding steps without endpoint-specific size enforcement. The parser decodes JSON from the request body, base64-decodes input.Content, and unmarshals the decoded blob. This path is invoked through the API dispatcher and can be abused with oversized payloads to trigger excessive allocations and resource exhaustion.

Impact: An authenticated user with API access can induce high memory consumption and instability, potentially causing degraded performance or process failure for other users.


AIS-MSO-GKP-016 - Authorization Bypass in File Add API

Severity: Medium Code: AIS-MSO-GKP-016

Description: The file upload API accepts a client-supplied fileRequestId and propagates it into persisted file metadata without validating ownership or authorization for the referenced request. The untrusted value is copied from multipart input into upload parameters and then stored. Quota logic later trusts UploadRequestId linkage to compute request usage, allowing malicious attribution of attacker uploads to another upload request.

Impact: An attacker can poison quota accounting for other upload requests, causing denial of service for legitimate file submissions and disruption of external intake workflows.


AIS-MSO-GKP-017 - Privilege Escalation in File Replace

Severity: Medium Code: AIS-MSO-GKP-017

Description: The file replacement API allows cross-user destructive behavior through insufficient authorization on idNewContent and deleteNewFile. Request headers provide attacker-controlled replacement source and deletion intent, and the cross-user gate only requires list visibility permission. The storage path can delete the referenced replacement file without additional authorization checks, enabling unauthorized cross-user file deletion.

Impact: A user with limited visibility privileges can trigger deletion of another user's file content, causing integrity loss and potential operational disruption in shared workflows.


AIS-MSO-GKP-018 - Privilege Escalation in API-Key Demotion

Severity: Medium Code: AIS-MSO-GKP-018

Description: User rank demotion does not fully revoke API-key permissions that were previously granted, allowing stale privileged keys to remain effective. API authorization decisions are made from key permission bits, and the demotion branch revokes only a subset of mapped capabilities. As a result, a previously issued key can continue to satisfy permission checks for upload-request management routes after the user is demoted.

Impact: Offboarded or demoted users can retain privileged API functionality longer than intended, weakening governance controls and allowing continued modification of upload-request resources.


AIS-MSO-GKP-019 - CSRF in Login Endpoint

Severity: Medium Code: AIS-MSO-GKP-019

Description: The login flow accepts credential-bearing requests without CSRF protection mechanisms tied to the browser session context. The handler parses form values directly and creates a session on successful credential validation. The login form template posts username and password fields but does not include a CSRF token field, so cross-site pages can trigger authentication state changes in a victim browser.

Impact: An attacker can force a victim browser into a session associated with attacker-controlled credentials, causing user confusion, activity misattribution, and potential misuse of trusted user actions.


AIS-MSO-GKP-020 - Stored XSS in SVG Hotlinks

Severity: Medium Code: AIS-MSO-GKP-020

Description: Uploaded SVG content is persisted and later served through a public hotlink path in a way that allows inline browser rendering. The upload pipeline accepts attacker-controlled file bytes and writes them to storage without sanitization, while SVG is treated as a hotlinkable image type. The hotlink handler serves the file publicly and response headers indicate inline disposition, enabling execution of active SVG script content in the application origin context.

Impact: A malicious uploader can distribute a hotlink that executes script in victims' browsers, enabling session abuse, unauthorized actions, and theft of sensitive in-app data available to the victim context.


AIS-MSO-GKP-004 - Auth Bypass in Disabled Mode

Severity: Low Code: AIS-MSO-GKP-004

Description: When authentication is disabled, the request authentication routine returns the super-admin account for incoming requests instead of enforcing credential checks. Protected routes rely on this decision and therefore treat unauthenticated traffic as authenticated administrative traffic.

Impact: If this mode is enabled in a reachable environment, any requester can operate the application as a super-admin. This can lead to full confidentiality, integrity, and availability compromise.


AIS-MSO-GKP-011 - TLS Key Path Overlap with Custom Directory

Severity: Low Code: AIS-MSO-GKP-011

Description: The application serves the local custom/ directory to unauthenticated users while configuration and TLS key paths are derived from configurable directory settings. If deployment configuration causes overlap between secret storage and the custom/ web root, sensitive files become publicly downloadable.

Impact: If deployment configuration causes overlap between secret storage and the custom/ web root, sensitive files become publicly downloadable.

Summary

AISafe was able to identify the SSE broadcast vulnerability, 1 DoS, and 3 unique logic bugs that resulted in CVEs, that the other tools haven’t discovered. Why? Well, looks like custom orchestration, knowledge base, guided threat modeling, and other secret sauces making up AISafe have big effect.

On the other side, the valid findings that other tools caught and AISafe did not, such as the file list vs. replace permission mismatch, the OIDC bug, and the file request cleanup typo, and some of the low severity findings. are worth putting in context: only the first and second have some security impact, even with the second one having serious preconditions necessary for exploitation. The low impact findings are better described as security improvements than actual vulnerabilities.


Summary

ProductFindingsVulnerabilities with direct impactWeaknesseses without direct impactAccepted riskFalse positives
Claude Code2418114
Claude Code w Skills2105214
Claude Code /security-review10100
Codex42200
Codex Security44618811
AISafe209470

Conclusion

So, back to our initial question: can a single prompt catch all the vulnerabilities in your application? Based on what we have seen today, no, and the gap between a raw LLM and a purpose-built security tool is not just about how many tokens you throw at the problem, but it comes down to how you spend those tokens. Flooding a report with low-impact findings and suggestions is easy; finding the complex logic issues is not.

At the end of the day, the results will always tell their own story. Security is not something you can afford to overlook. Fixing 45 reported issues might feel like a great achievement, but quality over quantity. That's why choosing the right tool matters just as much as choosing to act in the first place. And we hope that this case study has given you a clearer picture of where things stand today.

Quality over quantity is not just a nice principle in security research, it is the whole point why we strive to make quality accessible to everyone!

Check out our Code Audit service or get in touch directly.

CVEs & advisories

We'd also like to thank Gokapi's maintainer Forceu for a great collaboration on resolving and classifying the issues!

7 CVEs were assigned in total for the issues found by AISafe:

Share this post