Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .Jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## 2024-05-23 - API Security Hardening
**Vulnerability:** DoS risk via unlimited string input in `plan` API route, and potential rate limit bypass due to naive IP extraction.
**Learning:** Even with Zod, explicit `.max()` limits must be applied to all string inputs. IP extraction logic must handle `X-Forwarded-For` chains correctly (taking the first IP) to be robust against spoofing or proxy chains.
**Prevention:** Use a standardized `getIP` helper across all routes. Ensure every `z.string()` in API schemas has a reasonable `.max()` limit.
3 changes: 2 additions & 1 deletion src/app/api/architect/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
import { z } from "zod";
import { callOpenRouter, parseJSONResponse } from "@/lib/api/openrouter";
import { rateLimiter } from "@/lib/rate-limit";
import { getIP } from "@/lib/utils/ip";

// Schema for input validation
const ChatRequestSchema = z.object({
Expand Down Expand Up @@ -65,7 +66,7 @@ IMPORTANT:

export async function POST(req: Request) {
try {
const ip = req.headers.get("x-forwarded-for") || "unknown";
const ip = getIP(req);

if (!rateLimiter.check(ip)) {
return NextResponse.json(
Expand Down
7 changes: 4 additions & 3 deletions src/app/api/architect/plan/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { NextResponse } from "next/server";
import { z } from "zod";
import { callOpenRouter, parseJSONResponse } from "@/lib/api/openrouter";
import { rateLimiter } from "@/lib/rate-limit";
import { getIP } from "@/lib/utils/ip";
import toolsDB from "@/data/tools_database.json";
import bestPractices from "@/data/best_practices.json";

// Input: The User Request + The Tool ID they selected
const PlanRequestSchema = z.object({
userRequest: z.string(),
selectedToolId: z.string()
userRequest: z.string().max(5000), // Limit input length to prevent DoS
selectedToolId: z.string().max(100)
});

// Output: The Launch Plan
Expand All @@ -35,7 +36,7 @@ const PlanResponseSchema = z.object({

export async function POST(req: Request) {
try {
const ip = req.headers.get("x-forwarded-for") || "unknown";
const ip = getIP(req);

if (!rateLimiter.check(ip)) {
return NextResponse.json(
Expand Down
3 changes: 2 additions & 1 deletion src/app/api/architect/select/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { NextResponse } from "next/server";
import { callOpenRouter, parseJSONResponse } from "@/lib/api/openrouter";
import { rateLimiter } from "@/lib/rate-limit";
import { getIP } from "@/lib/utils/ip";
import { SelectionRequestSchema } from "@/types/selection";
import { filterCandidates } from "@/lib/selection/hard-filter";

const InputSchema = SelectionRequestSchema;

export async function POST(req: Request) {
try {
const ip = req.headers.get("x-forwarded-for") || "unknown";
const ip = getIP(req);

if (!rateLimiter.check(ip)) {
return NextResponse.json(
Expand Down
11 changes: 11 additions & 0 deletions src/lib/utils/ip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Extracts the client IP from the request headers.
* Handles x-forwarded-for headers with multiple proxies by taking the first IP.
*/
export function getIP(req: Request): string {
const forwardedFor = req.headers.get("x-forwarded-for");
if (forwardedFor) {
return forwardedFor.split(",")[0].trim();
}
return "unknown";
}