fix(security): sanitize agent-card topology, anti-spoof login rate-limit peer IP, and add 429 Retry-After (#S1 #S2 #S4) (#11418)

Validado em lote combinado (batch-0824g) contra o tip de release/v3.8.50: typecheck:core limpo, gates estáticos OK, 62/62 testes focados passando (S1/S2/S4, tests/unit/security-s1-s2-s4.test.ts, 9/9).

Boa integração com o padrão já existente de peer IP stamped por HMAC (resolveStampedPeer/OMNIROUTE_PEER_STAMP_TOKEN) — reusa em vez de reimplementar, e o header confiável só é honrado quando o stamp token está configurado. S2 remove corretamente a disclosure de topologia hardcoded do agent-card. Obrigado pela contribuição!
This commit is contained in:
Bob.Hou
2026-08-24 16:24:08 -04:00
committed by GitHub
parent 9464792cfc
commit 59dccdd9e1
8 changed files with 408 additions and 16 deletions

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { getAuditRequestContext, logAuditEvent } from "@/lib/compliance/index";
import { classifyIpScope } from "@/lib/ipUtils";
import { getCachedSettings } from "@/lib/db/settings";
@@ -13,6 +14,7 @@ import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
import { loginSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { checkLoginGuard, clearLoginAttempts, recordLoginFailure } from "@/server/auth/loginGuard";
import { AUTHZ_HEADER_TRUSTED_PEER_IP } from "@/server/authz/headers";
// SECURITY: No hardcoded fallback — JWT_SECRET must be configured.
if (!process.env.JWT_SECRET) {
@@ -28,7 +30,7 @@ export const authRouteInternals = {
getCookieStore: cookies,
};
export async function POST(request) {
export async function POST(request: NextRequest) {
const auditContext = getAuditRequestContext(request);
try {
@@ -75,7 +77,10 @@ export async function POST(request) {
return NextResponse.json({ error: "Invalid password payload" }, { status: 400 });
}
const settings = await getCachedSettings();
const clientIp = auditContext.ipAddress || null;
const trustedPeerIp = process.env.OMNIROUTE_PEER_STAMP_TOKEN
? request.headers.get(AUTHZ_HEADER_TRUSTED_PEER_IP)
: null;
const clientIp = trustedPeerIp || auditContext.ipAddress || null;
const oidcDisabledPassword =
settings.oidcEnabled === true &&
(settings.oidcDisablePasswordLogin === true ||
@@ -118,9 +123,7 @@ export async function POST(request) {
{ error: "Too many failed attempts. Try again later." },
{
status: 429,
headers: guardCheck.retryAfterSeconds
? { "Retry-After": String(guardCheck.retryAfterSeconds) }
: {},
headers: { "Retry-After": String(guardCheck.retryAfterSeconds || 60) },
}
);
}
@@ -220,9 +223,7 @@ export async function POST(request) {
{ error: "Too many failed attempts. Try again later." },
{
status: 429,
headers: failureDecision.retryAfterSeconds
? { "Retry-After": String(failureDecision.retryAfterSeconds) }
: {},
headers: { "Retry-After": String(failureDecision.retryAfterSeconds || 60) },
}
);
}