From 793e65fc6b438ec6e6f942b6495d3de998b6c3ad Mon Sep 17 00:00:00 2001 From: OG T Date: Sun, 7 Jun 2026 16:12:22 +0800 Subject: [PATCH] chore: harden external traffic attribution and clean external monitor output --- apps/web/src/app/api/mcp/[tool]/route.ts | 48 +++++++++++++++++++---- apps/web/src/app/api/open-tasks/route.ts | 50 +++++++++++++++++++----- apps/web/src/app/api/traffic/route.ts | 33 +++++++++++++++- 3 files changed, 113 insertions(+), 18 deletions(-) diff --git a/apps/web/src/app/api/mcp/[tool]/route.ts b/apps/web/src/app/api/mcp/[tool]/route.ts index e3b385f..44b3fab 100644 --- a/apps/web/src/app/api/mcp/[tool]/route.ts +++ b/apps/web/src/app/api/mcp/[tool]/route.ts @@ -7,6 +7,7 @@ import { TaskStatus, JudgeOverallResult } from "@agent-bounty/contracts"; +import { isIP } from "node:net"; import { prisma } from "@/lib/prisma"; import { runSubmissionInSandbox } from "@/lib/sandbox"; import { logAuditEvent } from "@/lib/audit"; @@ -28,20 +29,55 @@ const MCP_AGENT_HEADERS = [ "x-openai-agent", ]; +function sanitizeIpAddress(value: string | undefined) { + if (!value) { + return undefined; + } + + const first = value.split(",")[0]?.trim(); + if (!first) { + return undefined; + } + + const bracketedMatch = first.match(/^\[(.+)\]:(\d+)$/); + if (bracketedMatch?.[1]) { + return bracketedMatch[1].toLowerCase(); + } + + const ipv4WithPortMatch = first.match(/^(\d{1,3}(?:\.\d{1,3}){3}):\d+$/); + if (ipv4WithPortMatch?.[1]) { + return ipv4WithPortMatch[1]; + } + + return first.toLowerCase(); +} + function resolveSourceIp(request: NextRequest) { - return ( - request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? + return sanitizeIpAddress( + request.headers.get("x-forwarded-for") ?? request.headers.get("x-real-ip") ?? + request.headers.get("cf-connecting-ip") ?? + request.headers.get("true-client-ip") ?? + request.headers.get("x-client-ip") ?? "unknown" ); } function isPrivateIp(ip: string | undefined) { - if (!ip) return false; + if (!ip) return true; const normalized = ip.trim().toLowerCase(); - if (!normalized || normalized === "unknown" || normalized === "::1" || normalized === "localhost") { + if (!normalized || normalized === "unknown" || normalized === "localhost") { return true; } + if (normalized === "::1" || normalized.startsWith("fe80")) { + return true; + } + if (isIP(normalized) === 6 && (normalized.startsWith("fc") || normalized.startsWith("fd"))) { + return true; + } + if (isIP(normalized) === 0) { + return false; + } if (normalized.startsWith("127.") || normalized.startsWith("10.") || normalized.startsWith("192.168.")) { return true; @@ -52,10 +88,6 @@ function isPrivateIp(ip: string | undefined) { return secondOctet >= 16 && secondOctet <= 31; } - if (normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe80")) { - return true; - } - return false; } diff --git a/apps/web/src/app/api/open-tasks/route.ts b/apps/web/src/app/api/open-tasks/route.ts index 2a1d1ce..b51bb82 100644 --- a/apps/web/src/app/api/open-tasks/route.ts +++ b/apps/web/src/app/api/open-tasks/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; import { TaskStatus } from "@agent-bounty/contracts"; import { sendTrafficAlert } from "@/lib/traffic-alert"; +import { isIP } from "node:net"; export const dynamic = "force-dynamic"; @@ -41,21 +42,56 @@ function normalizeActorId(value: string, fallback: string) { return cleaned.slice(0, 64) || fallback; } +function sanitizeIpAddress(value: string | undefined) { + if (!value) { + return undefined; + } + + const first = value.split(",")[0]?.trim(); + if (!first) { + return undefined; + } + + const bracketedMatch = first.match(/^\[(.+)\]:(\d+)$/); + if (bracketedMatch?.[1]) { + return bracketedMatch[1].toLowerCase(); + } + + const ipv4WithPortMatch = first.match(/^(\d{1,3}(?:\.\d{1,3}){3}):\d+$/); + if (ipv4WithPortMatch?.[1]) { + return ipv4WithPortMatch[1]; + } + + return first.toLowerCase(); +} + function resolveSourceIp(request: Request) { - return ( - request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? + return sanitizeIpAddress( + request.headers.get("x-forwarded-for") ?? request.headers.get("x-real-ip") ?? + request.headers.get("cf-connecting-ip") ?? + request.headers.get("true-client-ip") ?? + request.headers.get("x-client-ip") ?? "unknown" ); } function isPrivateIp(ip: string | undefined) { - if (!ip) return false; + if (!ip) return true; const normalized = ip.trim().toLowerCase(); if (!normalized) { - return false; + return true; } - if (normalized === "::1" || normalized === "localhost") { + if (normalized === "unknown" || normalized === "localhost") { + return true; + } + if (normalized === "::1" || normalized.startsWith("fe80")) { + return true; + } + if (isIP(normalized) === 6 && (normalized.startsWith("fc") || normalized.startsWith("fd"))) { + return true; + } + if (isIP(normalized) === 0) { return false; } @@ -68,10 +104,6 @@ function isPrivateIp(ip: string | undefined) { return secondOctet >= 16 && secondOctet <= 31; } - if (normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe80")) { - return true; - } - return false; } diff --git a/apps/web/src/app/api/traffic/route.ts b/apps/web/src/app/api/traffic/route.ts index 2582e20..7be93a3 100644 --- a/apps/web/src/app/api/traffic/route.ts +++ b/apps/web/src/app/api/traffic/route.ts @@ -82,11 +82,42 @@ export async function GET(request: NextRequest) { actorSummaryRows.map((row) => [row.actorType, row._count._all]) ); + const isInternalActorId = (actorId: string | null | undefined) => { + if (!actorId) return true; + const normalized = actorId.toLowerCase(); + + if (normalized === "unknown" || normalized === "mcp-anonymous") { + return true; + } + + const ipMatch = normalized.match(/^open-tasks:([a-z0-9.:_-]+)$/); + if (!ipMatch?.[1]) { + return false; + } + + const actorIp = ipMatch[1]; + if (actorIp.startsWith("127.") || actorIp.startsWith("10.") || actorIp.startsWith("192.168.")) { + return true; + } + + if (actorIp.startsWith("172.")) { + const secondOctet = Number(actorIp.split(".")[1]); + return secondOctet >= 16 && secondOctet <= 31; + } + + if (actorIp === "localhost" || actorIp === "unknown" || actorIp.startsWith("fc") || actorIp.startsWith("fd")) { + return true; + } + + return false; + }; + const externalActorSummary = externalActorRows .map((row) => ({ actorId: row.actorId || "unknown", events: row._count._all, })) + .filter((row) => !isInternalActorId(row.actorId)) .sort((a, b) => b.events - a.events) .slice(0, 20); @@ -127,7 +158,7 @@ export async function GET(request: NextRequest) { }); const recentExternalEvents = recentEvents.filter((event) => - event.action.startsWith("EXTERNAL_") + event.action.startsWith("EXTERNAL_") && !isInternalActorId(event.actorId) ); const recentInternalEvents = recentEvents.filter(