Skip to content

Fix rate-limit bypass - #645

Merged
leoisadev1 merged 44 commits into
mainfrom
tembo/fix/rate-limit-bypass-trust-proxy-1
Feb 14, 2026
Merged

Fix rate-limit bypass#645
leoisadev1 merged 44 commits into
mainfrom
tembo/fix/rate-limit-bypass-trust-proxy-1

Conversation

@tembo

@tembo tembo Bot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixed potential rate-limit bypass vulnerability when TRUST_PROXY=true by:

  • Added IP format validation (isValidIpFormat()) to reject malformed/spoofed IPs using IPv4/IPv6 regex patterns
  • Prioritized platform-specific headers (cf-connecting-ip, x-vercel-forwarded-for, x-real-ip) over user-controlled x-forwarded-for
  • Applied IP validation to all proxy modes (cloudflare, vercel, true)
  • Enhanced warning message for TRUST_PROXY=true mode to highlight security risks
  • Made x-forwarded-for a last-resort fallback with validation

Files modified:

  • apps/web/src/routes/api/models.ts

Want tembo to make any changes? Add a review or comment with @tembo and i'll get back to work!

View on Tembo View Agent Settings


Summary by cubic

Fixes a rate‑limit bypass when TRUST_PROXY is enabled by validating client IPs and prioritizing trusted proxy headers. Adds IP-based rate limiting to /api/models and hardens OpenRouter fetch with timeouts, caching, and 502 handling.

  • Bug Fixes
    • Added IPv4/IPv6 checks (isValidIpFormat) for all proxy modes.
    • Prefer cf-connecting-ip, x-vercel-forwarded-for, then x-real-ip; use x-forwarded-for only as a validated last resort.
    • Added IP rate limit (30 req/60s) and safer OpenRouter fetch (10s timeout, 502 on failure, Redis cache); improved TRUST_PROXY warnings and fail-closed when unset/invalid.

Written for commit 8309c31. Summary will update on new commits.

leoisadev1 and others added 30 commits February 11, 2026 11:30
…o add-upstash-rate-limits-and

# Conflicts:
#	apps/web/src/hooks/use-persistent-chat.ts
#	apps/web/src/routes/api/typing.ts
Co-authored-by: cubic[bot] <cubic[bot]@users.noreply.github.com>
Co-authored-by: cubic[bot] <cubic[bot]@users.noreply.github.com>
- cleanup.ts: require token unconditionally, timing-safe compare, off-by-one fix
- crons.ts: timing-safe token comparison
- generate-title.ts: remove x-convex-token forwarding to QStash
- delete-account.ts: remove x-convex-token forwarding and trust
- export-chat.ts: remove x-convex-token forwarding and trust
- backgroundStream.ts: restore increment-before-search TOCTOU fix, strict maxSteps validation
- server-auth.ts: don't fall through to cookie on 5xx errors
- cleanup.ts: compare buffer byte lengths in safeCompare (non-ASCII safety)
- server-auth.ts: only return null on 4xx, let 5xx fall through to cookie
- crons.ts: avoid leaking token length in constant-time comparison
- cleanup.ts: constant-time safeCompare without length leak, stop forwarding
  WORKFLOW_CLEANUP_TOKEN to QStash
- delete-account.ts: bounded batch loops (MAX_DELETE_BATCHES=500), rate limiting
- export-chat.ts: apply exportRatelimit before processing
- generate-title.ts: add rate limiting before workflow trigger
- upstashUsage.ts: validate dateKey to prevent NaN/forever-TTL keys
- check-redis.ts: add 5s fetch timeout
- server/package.json: quote $VERCEL_GIT_COMMIT_REF
- upstash.ts: remove unused chatIpRatelimit/chatRatelimit exports
Replace hand-rolled XOR loop with crypto.createHmac('sha256') +
timingSafeEqual for verified constant-time comparison in Node.js runtime.
…s and error handling

- Move runCleanupBatchForWorkflow to cleanupAction.ts with 'use node' for
  crypto.timingSafeEqual (fixes hand-rolled XOR timing side-channel in crons.ts)
- Keep as public action() so ConvexHttpClient can call it (token-gated via HMAC)
- Fix server-auth.ts: return null when token endpoint returns 200 OK with no token
  (prevents auth bypass via cookie fallback)
- Move getMidnightUtcEpochSeconds inside try/catch in upstashUsage.ts
- Update cleanup.ts workflow to reference new api.cleanupAction path
- Remove stale comment from crons.ts
…nd bounds validation

- Remove session cookie forwarding from all 3 workflow trigger headers
  (generate-title, export-chat, delete-account) — cookies no longer sent
  to third-party QStash infrastructure
- Pass pre-resolved auth token via payload instead of cookie headers
- Remove getAuthTokenFromWorkflowHeaders from all workflow files
- Add IP-based rate limiting to /api/models endpoint (30 req/60s)
- Add 10s fetch timeout to OpenRouter models call
- Fix OpenRouter proxy: use Accept header instead of Content-Type on GET,
  return 502 for upstream errors instead of forwarding status codes
- Fix backgroundStream.ts double-counting: separate Convex mutation retry
  from Upstash increment to prevent billing double-charge on retry
- Add bounds validation to cleanupAction.ts (retentionDays 1-3650,
  batchSize 1-1000)
- Use env-based HMAC key in cleanupAction.ts safeCompare
- Log warning when Upstash Redis is not configured (rate limiting disabled)
…ndle fetch errors

- Fix P0: all 3 workflow payload parsers now pass through authToken field
  (generate-title, export-chat, delete-account) — prevents 'Unauthorized'
  on every workflow execution
- Remove hardcoded HMAC key from safeCompare in cleanup.ts and
  cleanupAction.ts — key is now passed as parameter from the validated
  env var, eliminating static key in source code
- Wrap fetchModelsFromOpenRouter in try/catch to handle timeout and
  network errors gracefully (returns 502 instead of unhandled 500)
- Use Math.ceil for sub-cent usageCents in upstashUsage.ts to prevent
  silent usage loss from Math.floor(0.5) = 0
- Add Number.isFinite() checks to retentionDays and batchSize validation
  in cleanupAction.ts (NaN bypasses comparison operators)
- Log OpenRouter fetch failures in models.ts catch block for observability
…odel endpoints

- Replace QStash payload authToken with short-lived authTokenRef keys stored in Upstash Redis for generate-title/export-chat/delete-account workflows
- Make cleanup batch executor internalAction and add Convex HTTP bridge endpoint to avoid public action exposure
- Harden /api/models with trusted IP extraction, missing-IP rejection, and fully wrapped upstream fetch/body handling
- Fix stream meta parsing for Upstash auto-deserialized objects and batch delete readStatus/promptTemplates in users cleanup
- Add workflow payload validation, IPv6 localhost support, and clean indentation issues flagged by reviews
…handling

- Encrypt workflow auth tokens at rest and consume via getdel with strict key-prefix validation
- Harden Convex cleanup batch HTTP bridge with typed error status mapping
- Require full QStash config (URL+token) and add production misconfig warning
- Tighten models IP derivation with trust-proxy gating and Retry-After on 429
- Add provisional atomic usage reservation/adjustment path to reduce daily-limit race window
- Normalize IPv6 localhost checks and finish remaining formatting/alignment fixes
* fix(web): validate chat ownership before Redis stream init

Add Convex-based chat ownership validation in both GET and POST handlers
of /api/chat to prevent unauthorized Redis stream access. Previously,
an authenticated user could use another user's chatId to overwrite
stream metadata and read streaming tokens.

- GET handler: validate ownership via api.chats.get before reading stream
- POST handler: validate ownership via api.chats.get before redis.stream.init

Closes OSS-22

* fix(api): add chat ownership validation before redis stream operations

* fix(chat): reuse convex client to eliminate redundant http request

---------

Co-authored-by: Tembo <tembo@opencode.ai>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
leoisadev1 and others added 13 commits February 12, 2026 00:19
- Remove dead jonMode/dynamicPrompt fields from validator, schema, client store, hooks, and settings UI
- Use exact-match error classification in http.ts instead of fragile substring matching
- Fix inconsistent indentation in chats.ts JSON.stringify
- Promote chatReadStatuses/promptTemplates deletion to workflow steps for proper transaction isolation
- Deprecate deleteAccount mutation in favor of workflow-based deleteAccountWorkflowStep
- Use static error message in app-sidebar toast instead of raw error.message
- Fix DELETE rate-limit message in openrouter-key.ts
Add Upstash rate limits and secure auto chat titles
…isting documents

Existing production streamJobs documents contain these fields. Convex
schema validation rejects documents with extra fields, so they must
remain in the schema until a migration removes them from all documents.
@tembo tembo Bot added the tembo Pull request created by Tembo label Feb 14, 2026
@tembo
tembo Bot requested a review from leoisadev1 February 14, 2026 00:08
@railway-app

railway-app Bot commented Feb 14, 2026

Copy link
Copy Markdown

This PR was not deployed automatically as @tembo[bot] does not have access to the Railway project.

In order to get automatic PR deploys, please add @tembo[bot] to your workspace on Railway.

@tembo

tembo Bot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor Author

Requesting review from @leoisadev1 who has experience with the following files modified in this PR:

  • apps/web/src/routes/api/models.ts

@leoisadev1
leoisadev1 marked this pull request as ready for review February 14, 2026 02:29

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

17 issues found across 43 files

Confidence score: 2/5

  • High-risk logic issues: apps/web/src/routes/api/workflow/generate-title.ts makes the OpenRouter fetch outside context.run()/call(), so retries/replays can re-execute business logic unexpectedly.
  • Concurrency/data correctness risk in apps/server/convex/lib/upstashUsage.ts where the floor-to-zero correction can erase a concurrent INCRBY, leading to inaccurate usage tracking.
  • Additional user-facing and validation concerns (e.g., raw error exposure in apps/web/src/hooks/use-persistent-chat.ts, unsafe userId cast in apps/web/src/routes/api/workflow/export-chat.ts, and overly permissive IPv6 regex in apps/web/src/routes/api/models.ts) add uncertainty.
  • Pay close attention to apps/web/src/routes/api/workflow/generate-title.ts, apps/server/convex/lib/upstashUsage.ts, apps/web/src/hooks/use-persistent-chat.ts, apps/web/src/routes/api/workflow/export-chat.ts, apps/web/src/routes/api/models.ts - replays/races and user-facing validation/errors.
Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/server/convex/users.ts">

<violation number="1" location="apps/server/convex/users.ts:870">
P2: `batchSize` is not forwarded to `deleteUserChatReadStatuses` here, unlike all other batch-delete steps in this action. The underlying mutation accepts `batchSize`, so this appears to be an accidental omission.</violation>

<violation number="2" location="apps/server/convex/users.ts:874">
P2: `batchSize` is not forwarded to `deleteUserPromptTemplates` here, unlike all other batch-delete steps in this action.</violation>
</file>

<file name="apps/web/src/routes/api/workflow/generate-title.ts">

<violation number="1" location="apps/web/src/routes/api/workflow/generate-title.ts:246">
P1: The OpenRouter API `fetch` is not wrapped in `context.run()` or `context.call()`, so it will be re-executed on every workflow replay/retry. According to the Upstash Workflow docs, all business logic must be inside `context.run()` steps, and `context.call()` is specifically designed for HTTP requests as workflow steps. Use `context.call()` here so the request is executed once and the result is durably stored.</violation>
</file>

<file name="apps/web/src/hooks/use-persistent-chat.ts">

<violation number="1" location="apps/web/src/hooks/use-persistent-chat.ts:1126">
P2: Inconsistent error sanitization: `forkMessage` still exposes raw `parsedError.message` to the user while `editMessage` and `retryMessage` were updated to use `getUserFriendlyError()`. This could leak internal error details (API structures, DB field names, etc.).</violation>
</file>

<file name="apps/server/convex/crons.ts">

<violation number="1" location="apps/server/convex/crons.ts:77">
P3: Error message is misleading: validation rejects non-integer values (e.g. `1.5`) but the message only mentions the range constraint. Include the integer requirement so callers can understand why their input was rejected.</violation>

<violation number="2" location="apps/server/convex/crons.ts:80">
P3: Same misleading error message: validation rejects non-integer values but the message only mentions the range. Include the integer requirement.</violation>
</file>

<file name="apps/server/convex/lib/sanitize.ts">

<violation number="1" location="apps/server/convex/lib/sanitize.ts:44">
P2: HTML tag stripping is ordered after space collapsing, so removing tags can leave behind uncollapsed multiple spaces (e.g., `"hello <b> world </b> test"` → `"hello  world  test"`). Move this line before the space-collapse step, or re-collapse spaces afterward.</violation>
</file>

<file name="apps/server/convex/lib/upstashUsage.ts">

<violation number="1" location="apps/server/convex/lib/upstashUsage.ts:189">
P1: Race condition: the floor-to-zero correction reads the total from one pipeline then issues `SET key 0` in a separate pipeline. A concurrent `INCRBY` between the two calls will be silently erased. Consider using a Lua script (Upstash supports `EVAL`) to atomically clamp the value, e.g.:
```lua
local v = redis.call('INCRBY', KEYS[1], ARGV[1])
if v < 0 then redis.call('SET', KEYS[1], 0) end
redis.call('EXPIREAT', KEYS[1], ARGV[2])
return v
```</violation>
</file>

<file name="apps/web/src/lib/server-auth.ts">

<violation number="1" location="apps/web/src/lib/server-auth.ts:20">
P2: `ALLOW_AUTH_COOKIE_FALLBACK` is defined and guarded at module level but never consulted in the actual fallback path. The cookie fallback is unconditionally enabled in dev/test via `IS_LOCAL_DEV`. If this env var is meant to be an opt-in gate, it should be checked before using the fallback; otherwise, remove the dead variable and the module-level guard.</violation>
</file>

<file name="apps/web/src/routes/api/workflow/export-chat.ts">

<violation number="1" location="apps/web/src/routes/api/workflow/export-chat.ts:124">
P2: Unsafe `as Id<"users">` cast on a potentially `undefined` value. `ExportChatPayload.userId` is typed as `string | undefined`, so `userId` here can be `undefined`. The Convex query requires a valid `Id<"users">` and will throw a validation error at runtime if it receives `undefined`. Add an explicit check before the query call to fail with a clear error.</violation>
</file>

<file name="scripts/dev.ts">

<violation number="1" location="scripts/dev.ts:25">
P2: Local variable `process` shadows the global `process` object. Consider renaming to `subprocess`, `child`, or `proc` for clarity and to avoid accidental misuse if this function is later extended.</violation>
</file>

<file name="apps/web/src/components/app-sidebar.tsx">

<violation number="1" location="apps/web/src/components/app-sidebar.tsx:454">
P2: Fragile string-based error detection: matching against `message.toLowerCase().includes("too many title generations")` will silently break if the server-side error message text changes. Consider using a structured error property (e.g., `error.name`, a custom error code, or a `ConvexError` data field) instead of relying on substring matching on the human-readable message.</violation>
</file>

<file name="apps/server/convex/cleanupAction.ts">

<violation number="1" location="apps/server/convex/cleanupAction.ts:38">
P2: Validation mismatch: `Number.isFinite()` allows fractional values (e.g., `1.5`) that the downstream mutation rejects with `Number.isInteger()`. Use `Number.isInteger()` here to match the mutation's validation and surface a clear error message at the entry point.</violation>

<violation number="2" location="apps/server/convex/cleanupAction.ts:42">
P2: Same validation mismatch as `retentionDays`: `Number.isFinite()` should be `Number.isInteger()` to match the downstream mutation's validation.</violation>
</file>

<file name="apps/web/src/routes/api/models.ts">

<violation number="1" location="apps/web/src/routes/api/models.ts:16">
P2: IPv4 regex doesn't validate octet ranges (0–255), so values like `999.999.999.999` pass validation. Consider using Node.js `net.isIP(ip) !== 0` which validates both IPv4 and IPv6 correctly, replacing both regex patterns with a single robust check.</violation>

<violation number="2" location="apps/web/src/routes/api/models.ts:17">
P2: IPv6 regex is too permissive — it matches any hex string (e.g., `a`, `deadbeef`, `:`) as a valid IPv6 address. Since this validation exists specifically to reject spoofed/malformed values, accepting arbitrary hex strings undermines its purpose. At minimum, require the presence of a colon, or better yet use Node's built-in `net.isIP()` which handles both IPv4 and IPv6 correctly.</violation>
</file>

<file name="apps/server/convex/chats.ts">

<violation number="1" location="apps/server/convex/chats.ts:571">
P3: Indentation mismatch: `body` property is indented one level less than sibling properties `method` and `headers` in the `fetch` options object. This makes the object structure visually misleading. The `body` and closing `});` should be indented to match `method` and `headers`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

TITLE_STYLE_PROMPTS[length],
].join(" ");

let llmResponseStatus = 0;

@cubic-dev-ai cubic-dev-ai Bot Feb 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The OpenRouter API fetch is not wrapped in context.run() or context.call(), so it will be re-executed on every workflow replay/retry. According to the Upstash Workflow docs, all business logic must be inside context.run() steps, and context.call() is specifically designed for HTTP requests as workflow steps. Use context.call() here so the request is executed once and the result is durably stored.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/routes/api/workflow/generate-title.ts, line 246:

<comment>The OpenRouter API `fetch` is not wrapped in `context.run()` or `context.call()`, so it will be re-executed on every workflow replay/retry. According to the Upstash Workflow docs, all business logic must be inside `context.run()` steps, and `context.call()` is specifically designed for HTTP requests as workflow steps. Use `context.call()` here so the request is executed once and the result is durably stored.</comment>

<file context>
@@ -0,0 +1,438 @@
+		TITLE_STYLE_PROMPTS[length],
+	].join(" ");
+
+	let llmResponseStatus = 0;
+	let llmResponseBody: { choices?: Array<{ message?: { content?: string } }> } | null = null;
+	try {
</file context>
Fix with Cubic

: typeof total === "string"
? Number.parseInt(total, 10)
: null;
if (typeof parsedTotal === "number" && Number.isFinite(parsedTotal) && parsedTotal < 0) {

@cubic-dev-ai cubic-dev-ai Bot Feb 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Race condition: the floor-to-zero correction reads the total from one pipeline then issues SET key 0 in a separate pipeline. A concurrent INCRBY between the two calls will be silently erased. Consider using a Lua script (Upstash supports EVAL) to atomically clamp the value, e.g.:

local v = redis.call('INCRBY', KEYS[1], ARGV[1])
if v < 0 then redis.call('SET', KEYS[1], 0) end
redis.call('EXPIREAT', KEYS[1], ARGV[2])
return v
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/convex/lib/upstashUsage.ts, line 189:

<comment>Race condition: the floor-to-zero correction reads the total from one pipeline then issues `SET key 0` in a separate pipeline. A concurrent `INCRBY` between the two calls will be silently erased. Consider using a Lua script (Upstash supports `EVAL`) to atomically clamp the value, e.g.:
```lua
local v = redis.call('INCRBY', KEYS[1], ARGV[1])
if v < 0 then redis.call('SET', KEYS[1], 0) end
redis.call('EXPIREAT', KEYS[1], ARGV[2])
return v
```</comment>

<file context>
@@ -0,0 +1,200 @@
+				: typeof total === "string"
+					? Number.parseInt(total, 10)
+					: null;
+		if (typeof parsedTotal === "number" && Number.isFinite(parsedTotal) && parsedTotal < 0) {
+			await executePipeline([
+				["SET", key, 0],
</file context>
Fix with Cubic

Comment on lines +874 to +877
case "delete-prompt-templates":
return await ctx.runMutation(internal.users.deleteUserPromptTemplates, {
userId,
});

@cubic-dev-ai cubic-dev-ai Bot Feb 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: batchSize is not forwarded to deleteUserPromptTemplates here, unlike all other batch-delete steps in this action.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/convex/users.ts, line 874:

<comment>`batchSize` is not forwarded to `deleteUserPromptTemplates` here, unlike all other batch-delete steps in this action.</comment>

<file context>
@@ -598,18 +596,309 @@ export const updateName = mutation({
+				return await ctx.runMutation(internal.users.deleteUserChatReadStatuses, {
+					userId,
+				});
+			case "delete-prompt-templates":
+				return await ctx.runMutation(internal.users.deleteUserPromptTemplates, {
+					userId,
</file context>
Suggested change
case "delete-prompt-templates":
return await ctx.runMutation(internal.users.deleteUserPromptTemplates, {
userId,
});
case "delete-prompt-templates":
return await ctx.runMutation(internal.users.deleteUserPromptTemplates, {
userId,
batchSize: args.batchSize,
});
Fix with Cubic

Comment on lines +870 to +873
case "delete-chat-read-statuses":
return await ctx.runMutation(internal.users.deleteUserChatReadStatuses, {
userId,
});

@cubic-dev-ai cubic-dev-ai Bot Feb 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: batchSize is not forwarded to deleteUserChatReadStatuses here, unlike all other batch-delete steps in this action. The underlying mutation accepts batchSize, so this appears to be an accidental omission.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/convex/users.ts, line 870:

<comment>`batchSize` is not forwarded to `deleteUserChatReadStatuses` here, unlike all other batch-delete steps in this action. The underlying mutation accepts `batchSize`, so this appears to be an accidental omission.</comment>

<file context>
@@ -598,18 +596,309 @@ export const updateName = mutation({
+					userId,
+					batchSize: args.batchSize,
+				});
+			case "delete-chat-read-statuses":
+				return await ctx.runMutation(internal.users.deleteUserChatReadStatuses, {
+					userId,
</file context>
Suggested change
case "delete-chat-read-statuses":
return await ctx.runMutation(internal.users.deleteUserChatReadStatuses, {
userId,
});
case "delete-chat-read-statuses":
return await ctx.runMutation(internal.users.deleteUserChatReadStatuses, {
userId,
batchSize: args.batchSize,
});
Fix with Cubic

} catch (err) {
const parsedError = err instanceof Error ? err : new Error("Unknown error");
toast.error("Failed to branch off", {
description: parsedError.message,

@cubic-dev-ai cubic-dev-ai Bot Feb 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Inconsistent error sanitization: forkMessage still exposes raw parsedError.message to the user while editMessage and retryMessage were updated to use getUserFriendlyError(). This could leak internal error details (API structures, DB field names, etc.).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/hooks/use-persistent-chat.ts, line 1126:

<comment>Inconsistent error sanitization: `forkMessage` still exposes raw `parsedError.message` to the user while `editMessage` and `retryMessage` were updated to use `getUserFriendlyError()`. This could leak internal error details (API structures, DB field names, etc.).</comment>

<file context>
@@ -1146,47 +1103,43 @@ export function usePersistentChat({
+		} catch (err) {
+			const parsedError = err instanceof Error ? err : new Error("Unknown error");
+			toast.error("Failed to branch off", {
+				description: parsedError.message,
+			});
+			return undefined;
</file context>
Fix with Cubic

* Basic IPv4/IPv6 format validation.
* Rejects obviously spoofed or malformed values used in x-forwarded-for.
*/
const IPV4_REGEX = /^(\d{1,3}\.){3}\d{1,3}$/;

@cubic-dev-ai cubic-dev-ai Bot Feb 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: IPv4 regex doesn't validate octet ranges (0–255), so values like 999.999.999.999 pass validation. Consider using Node.js net.isIP(ip) !== 0 which validates both IPv4 and IPv6 correctly, replacing both regex patterns with a single robust check.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/routes/api/models.ts, line 16:

<comment>IPv4 regex doesn't validate octet ranges (0–255), so values like `999.999.999.999` pass validation. Consider using Node.js `net.isIP(ip) !== 0` which validates both IPv4 and IPv6 correctly, replacing both regex patterns with a single robust check.</comment>

<file context>
@@ -0,0 +1,199 @@
+ * Basic IPv4/IPv6 format validation.
+ * Rejects obviously spoofed or malformed values used in x-forwarded-for.
+ */
+const IPV4_REGEX = /^(\d{1,3}\.){3}\d{1,3}$/;
+const IPV6_REGEX = /^[0-9a-fA-F:]+$/;
+
</file context>
Fix with Cubic

* Rejects obviously spoofed or malformed values used in x-forwarded-for.
*/
const IPV4_REGEX = /^(\d{1,3}\.){3}\d{1,3}$/;
const IPV6_REGEX = /^[0-9a-fA-F:]+$/;

@cubic-dev-ai cubic-dev-ai Bot Feb 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: IPv6 regex is too permissive — it matches any hex string (e.g., a, deadbeef, :) as a valid IPv6 address. Since this validation exists specifically to reject spoofed/malformed values, accepting arbitrary hex strings undermines its purpose. At minimum, require the presence of a colon, or better yet use Node's built-in net.isIP() which handles both IPv4 and IPv6 correctly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/routes/api/models.ts, line 17:

<comment>IPv6 regex is too permissive — it matches any hex string (e.g., `a`, `deadbeef`, `:`) as a valid IPv6 address. Since this validation exists specifically to reject spoofed/malformed values, accepting arbitrary hex strings undermines its purpose. At minimum, require the presence of a colon, or better yet use Node's built-in `net.isIP()` which handles both IPv4 and IPv6 correctly.</comment>

<file context>
@@ -0,0 +1,199 @@
+ * Rejects obviously spoofed or malformed values used in x-forwarded-for.
+ */
+const IPV4_REGEX = /^(\d{1,3}\.){3}\d{1,3}$/;
+const IPV6_REGEX = /^[0-9a-fA-F:]+$/;
+
+function isValidIpFormat(ip: string): boolean {
</file context>
Fix with Cubic

throw new Error("retentionDays must be between 1 and 3650");
}
if (!Number.isInteger(batchSize) || batchSize < 1 || batchSize > 1000) {
throw new Error("batchSize must be between 1 and 1000");

@cubic-dev-ai cubic-dev-ai Bot Feb 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Same misleading error message: validation rejects non-integer values but the message only mentions the range. Include the integer requirement.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/convex/crons.ts, line 80:

<comment>Same misleading error message: validation rejects non-integer values but the message only mentions the range. Include the integer requirement.</comment>

<file context>
@@ -73,6 +73,12 @@ export const cleanupSoftDeletedRecords = internalMutation({
+			throw new Error("retentionDays must be between 1 and 3650");
+		}
+		if (!Number.isInteger(batchSize) || batchSize < 1 || batchSize > 1000) {
+			throw new Error("batchSize must be between 1 and 1000");
+		}
 
</file context>
Suggested change
throw new Error("batchSize must be between 1 and 1000");
throw new Error("batchSize must be an integer between 1 and 1000");
Fix with Cubic

const batchSize = args.batchSize ?? 100;
const dryRun = args.dryRun ?? false;
if (!Number.isInteger(retentionDays) || retentionDays < 1 || retentionDays > 3650) {
throw new Error("retentionDays must be between 1 and 3650");

@cubic-dev-ai cubic-dev-ai Bot Feb 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Error message is misleading: validation rejects non-integer values (e.g. 1.5) but the message only mentions the range constraint. Include the integer requirement so callers can understand why their input was rejected.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/convex/crons.ts, line 77:

<comment>Error message is misleading: validation rejects non-integer values (e.g. `1.5`) but the message only mentions the range constraint. Include the integer requirement so callers can understand why their input was rejected.</comment>

<file context>
@@ -73,6 +73,12 @@ export const cleanupSoftDeletedRecords = internalMutation({
 		const batchSize = args.batchSize ?? 100;
 		const dryRun = args.dryRun ?? false;
+		if (!Number.isInteger(retentionDays) || retentionDays < 1 || retentionDays > 3650) {
+			throw new Error("retentionDays must be between 1 and 3650");
+		}
+		if (!Number.isInteger(batchSize) || batchSize < 1 || batchSize > 1000) {
</file context>
Suggested change
throw new Error("retentionDays must be between 1 and 3650");
throw new Error("retentionDays must be an integer between 1 and 3650");
Fix with Cubic

"HTTP-Referer": process.env.CONVEX_SITE_URL || "https://osschat.io",
"X-Title": "OSSChat",
},
body: JSON.stringify({

@cubic-dev-ai cubic-dev-ai Bot Feb 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Indentation mismatch: body property is indented one level less than sibling properties method and headers in the fetch options object. This makes the object structure visually misleading. The body and closing }); should be indented to match method and headers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/convex/chats.ts, line 571:

<comment>Indentation mismatch: `body` property is indented one level less than sibling properties `method` and `headers` in the `fetch` options object. This makes the object structure visually misleading. The `body` and closing `});` should be indented to match `method` and `headers`.</comment>

<file context>
@@ -526,6 +526,87 @@ const TITLE_STYLE_PROMPTS: Record<"short" | "standard" | "long", string> = {
+				"HTTP-Referer": process.env.CONVEX_SITE_URL || "https://osschat.io",
+				"X-Title": "OSSChat",
+			},
+		body: JSON.stringify({
+			model: TITLE_MODEL_ID,
+			messages: [
</file context>
Suggested change
body: JSON.stringify({
body: JSON.stringify({
Fix with Cubic

@vercel

vercel Bot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
osschat-web Error Error Feb 14, 2026 2:57am

@leoisadev1
leoisadev1 merged commit cb03909 into main Feb 14, 2026
4 of 5 checks passed
@leoisadev1
leoisadev1 deleted the tembo/fix/rate-limit-bypass-trust-proxy-1 branch February 14, 2026 02:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tembo Pull request created by Tembo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant