fix(security): KB fileUrl LFI, MCP/Agiloft SSRF pinning, form OTP, KB authz#4639
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
️✅ There are no secrets present in this pull request anymore.If these secrets were true positive and are still valid, we highly recommend you to revoke them. 🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request. |
PR SummaryHigh Risk Overview Centralizes OTP storage/attempt tracking. Moves chat OTP logic into a shared Closes a KB document ingestion LFI vector. Tightens Pins and validates outbound connections to reduce SSRF/TOCTOU risk. MCP SSRF validation now returns a resolved IP and, when available, the MCP client uses an undici-based pinned fetch to prevent DNS rebinding; Agiloft attach/retrieve routes similarly resolve once and pin all subsequent requests; Grafana update tools now validate Hardens knowledge base workspace authorization. Adds Updates API validation baseline and adds extensive unit tests across the new and hardened paths. Reviewed by Cursor Bugbot for commit 21a93a5. Configure here. |
Greptile SummaryThis PR delivers a bundled security hardening across five surfaces: LFI prevention on Knowledge Base file upload, KB permission errors and transaction hardening, DNS-rebinding (SSRF) pinning for MCP and Agiloft, basic URL validation for Grafana tools, and a new email-OTP gate for form deployments with a shared
Confidence Score: 5/5Safe to merge; the Grafana URL validation gap does not regress from the pre-PR state and is noted for a follow-up improvement. The five hardened surfaces (KB LFI, KB authz, MCP DNS pinning, Agiloft DNS pinning, form OTP) are all correctly implemented. The Grafana tools move from no validation to basic format checking — an improvement, not a regression — but do not reach the DNS-pinned level of the Agiloft fix. All previous review comments were addressed in follow-up commits. The OTP module's Redis Lua path, DB optimistic-lock fail-closed path, and rate-limiting are sound. apps/sim/tools/grafana/update_alert_rule.ts and apps/sim/tools/grafana/update_dashboard.ts — both validate baseUrl with the synchronous format-only check and still use plain fetch; a follow-up to add DNS resolution and IP pinning would bring them in line with the Agiloft pattern.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/core/security/otp.ts | New shared OTP module: correct Redis Lua atomic increment, DB optimistic-lock retry with fail-closed exhaustion, per-kind key namespacing, and clean encode/decode for the code:attempts format. |
| apps/sim/lib/mcp/pinned-fetch.ts | New pinned-fetch implementation using undici's typed fetch export with a createPinnedLookup dispatcher; correctly bridges DOM/undici type gap via documented cast and preserves hostname for TLS SNI. |
| apps/sim/lib/mcp/domain-check.ts | Returns resolved IP from validateMcpServerSsrf for downstream pinning; loopback is now blocked on hosted environments; DNS failure path is now distinct from SSRF-block path. |
| apps/sim/tools/agiloft/utils.server.ts | New server-only Agiloft helpers: resolves DNS once via validateUrlWithDNS, then uses secureFetchWithPinnedIP for login/logout/attach/retrieve, closing the TOCTOU SSRF window. |
| apps/sim/tools/grafana/update_alert_rule.ts | Adds validateExternalUrl before the Grafana API call — blocks IP-literal SSRF but does not resolve DNS, leaving DNS-rebinding attacks open (inconsistent with the Agiloft/MCP pattern). |
| apps/sim/tools/grafana/update_dashboard.ts | Same as update_alert_rule — synchronous format-only validation with plain fetch; DNS rebinding SSRF not mitigated. |
| apps/sim/lib/knowledge/service.ts | KB update wrapped in a SELECT FOR UPDATE transaction; workspace-change permission check added via actorUserId; KnowledgeBasePermissionError distinguishes auth failures from 500s. |
| apps/sim/lib/knowledge/documents/document-processor.ts | Removes fs.readFile fallback; all non-data:/http(s):// paths now throw; case-insensitive regex checks applied consistently across downloadFileForBase64 and parseWithFileParser. |
| apps/sim/app/api/form/[identifier]/otp/route.ts | New form OTP route mirrors chat OTP pattern precisely; IP and email rate limiting, allowlist check before OTP store/compare, generic 500 messages, and auth cookie set on success. |
| apps/sim/lib/api/contracts/knowledge/shared.ts | Adds knowledgeDocumentFileUrlSchema with case-insensitive data: and https?:// guards; correctly applied at the Zod validation boundary. |
| apps/sim/app/form/[identifier]/components/email-auth.tsx | New email-auth UI component with two-step flow (email → OTP), countdown resend timer, 6-digit OTP auto-submit, and clear error messaging. |
Reviews (2): Last reviewed commit: "fix(mcp): annotate undici/DOM type-bridg..." | Re-trigger Greptile
…haust - Chat/form OTP routes: replace `error.message || fallback` with generic `Failed to process request` in 500 responses (logger still captures detail). - otp.ts incrementOTPAttempts DB path: on MAX_RETRIES exhaustion, delete the verification row and return `'locked'` instead of trusting a possibly- undercounted final read. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace `globalThis.fetch` + double-cast with `undici.fetch` so the `dispatcher` option is part of the real type contract. This guarantees pinning won't silently break if a future runtime swaps the underlying fetch implementation. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tool config files are statically reachable from the client bundle (via
tools/registry.ts → tools/{service}/index.ts). Importing
`@/lib/core/security/input-validation.server` from these files pulled
`node:dns/promises` into the Turbopack client bundle and broke the build.
Split agiloft utils into client-safe (`utils.ts`, plain fetch + sync
`validateExternalUrl`) and server-only (`utils.server.ts`, DNS-pinned
variants). Routes that need TOCTOU protection import the pinned helpers;
the executor-side tool path falls back to sync URL validation (matches
the supabase precedent and pre-PR baseline).
Grafana update tools likewise switch from `secureFetchWithValidation`
(server-only) to inline sync `validateExternalUrl` + plain fetch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Boundary schema accepted uppercase schemes (e.g. HTTPS://, DATA:) via the
case-insensitive http regex, but the processor's case-sensitive
startsWith('data:') / startsWith('http') / startsWith('https://') checks
rejected them with a confusing "Unsupported fileUrl scheme" error.
Aligns processor checks to the schema using case-insensitive regex per
RFC 3986 §3.1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Strict audit was failing on two new `as unknown as` casts in pinned-fetch.ts. They bridge DOM `RequestInit`/`Response` ↔ undici equivalents (structurally compatible at runtime since Node's global fetch is undici) and are required to satisfy the FetchLike contract. Annotate so they count as documented exemptions instead of new violations. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
abdc919 to
21a93a5
Compare
|
@greptile |
|
@cursor review |
|
@cursor review |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 21a93a5. Configure here.
| form: { | ||
| redisKey: (email: string, deploymentId: string) => `form-otp:${email}:${deploymentId}`, | ||
| dbIdentifier: (email: string, deploymentId: string) => `form-otp:${deploymentId}:${email}`, | ||
| }, |
There was a problem hiding this comment.
Form OTP key component ordering inconsistent across storage backends
Low Severity
The form OTP key format has reversed component ordering between Redis (form-otp:${email}:${deploymentId}) and DB (form-otp:${deploymentId}:${email}). For chat, this inconsistency is preserved for backward compatibility with in-flight OTPs (documented in the comment above). But form is an entirely new key format with no legacy data — there's no reason to replicate the asymmetry. This makes the code harder to reason about and could cause confusion when debugging OTP issues or if a deployment ever switches storage backends mid-flight.
Reviewed by Cursor Bugbot for commit 21a93a5. Configure here.


Summary
Bundled security hardening across multiple surfaces:
POST /api/knowledge/[id]/documents/upsertpreviously accepted any non-empty string forfileUrl, which the background processor then passed tofs.readFile/parseFile, allowing authenticated arbitrary local file reads (e.g./app/.env,/etc/passwd). Now gated at the boundary viaknowledgeDocumentFileUrlSchema(onlydata:URIs orhttp(s)://URLs); defense-in-depth indocument-processor.tsthrows on any other scheme.KnowledgeBasePermissionError, tightens permission checks, and wraps multi-step KB mutations in transactions.pinned-fetch.ts(undiciAgentwith a pinnedlookup) so the host the policy validated is the host actually connected to.resolveAgiloftInstance+ pinned fetch forattach/retrieveroutes.update_alert_rule/update_dashboardroute throughsecureFetchWithValidation.POST /api/form/[identifier]/otproute +email-auth.tsxUI; shared OTP module underlib/core/security/otp.tskeyed byDeploymentKind, with the chat OTP route refactored to consume it.Test plan
bun run check:api-validation:strictpasses/api/files/serve/...rows have zerocompletedstatus (already failing under the oldfs.readFilepath); change is strictly a clearer error, not a regression🤖 Generated with Claude Code