Skip to content

Commit f642eb9

Browse files
authored
Merge pull request #20 from Prismer-AI/fix/security-hardening
fix(security): auth on internal APIs, secure UUID, SSRF blocklist (#10 #12 #14)
2 parents b772a9f + 043129c commit f642eb9

9 files changed

Lines changed: 971 additions & 17 deletions

File tree

server/src/app/api/compress/route.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
22
import OpenAI from 'openai';
33
import { SOURCE_QUALIFIER_SYSTEM, getPromptForStrategy } from '@/lib/prompts';
44
import { ensureNacosConfig } from '@/lib/nacos-config';
5+
import { apiGuard } from '@/lib/api-guard';
56
import { metrics } from '@/lib/metrics';
67

78
// Token limit configuration
@@ -98,9 +99,12 @@ function truncateContent(content: string, maxChars: number = MAX_CONTENT_CHARS):
9899
export async function POST(request: NextRequest) {
99100
const reqStart = Date.now();
100101
try {
102+
const guard = await apiGuard(request, { tier: 'billable', estimatedCost: 1 });
103+
if (!guard.ok) return guard.response;
104+
101105
// Ensure Nacos config is loaded before accessing env vars
102106
await initNacos();
103-
107+
104108
const config = getOpenAIConfig();
105109

106110
if (!config.apiKey) {

server/src/app/api/content/route.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NextRequest, NextResponse } from 'next/server';
22
import Exa from 'exa-js';
33
import { ensureNacosConfig } from '@/lib/nacos-config';
4+
import { apiGuard } from '@/lib/api-guard';
45
import { metrics } from '@/lib/metrics';
56

67
// Initialize Nacos config on module load (singleton pattern)
@@ -32,9 +33,12 @@ function getContentApiKey(): string | undefined {
3233
export async function POST(request: NextRequest) {
3334
const reqStart = Date.now();
3435
try {
36+
const guard = await apiGuard(request, { tier: 'billable', estimatedCost: 1 });
37+
if (!guard.ok) return guard.response;
38+
3539
// Ensure Nacos config is loaded before accessing env vars
3640
await initNacos();
37-
41+
3842
const CONTENT_API_KEY = getContentApiKey();
3943

4044
if (!CONTENT_API_KEY) {
@@ -54,10 +58,34 @@ export async function POST(request: NextRequest) {
5458
);
5559
}
5660

57-
// Validate all URLs
61+
// Validate all URLs — block private/internal networks
5862
for (const url of urls) {
5963
try {
60-
new URL(url);
64+
const parsed = new URL(url);
65+
if (!['http:', 'https:'].includes(parsed.protocol)) {
66+
return NextResponse.json(
67+
{ error: `Invalid URL scheme: ${parsed.protocol} (only http/https allowed)` },
68+
{ status: 400 }
69+
);
70+
}
71+
const host = parsed.hostname.toLowerCase();
72+
if (
73+
host === 'localhost' ||
74+
host === '127.0.0.1' ||
75+
host === '::1' ||
76+
host === '0.0.0.0' ||
77+
host.startsWith('10.') ||
78+
host.startsWith('172.') ||
79+
host.startsWith('192.168.') ||
80+
host.startsWith('169.254.') ||
81+
host.endsWith('.internal') ||
82+
host.endsWith('.local')
83+
) {
84+
return NextResponse.json(
85+
{ error: `Blocked URL: private/internal network addresses are not allowed` },
86+
{ status: 400 }
87+
);
88+
}
6189
} catch {
6290
return NextResponse.json(
6391
{ error: `Invalid URL: ${url}` },

server/src/app/api/context/load/route.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ async function handleSingleUrl(
199199

200200
const contentRes = await fetch(`${baseUrl}/api/content`, {
201201
method: 'POST',
202-
headers: { 'Content-Type': 'application/json' },
202+
headers: { 'Content-Type': 'application/json', ...(authHeader ? { 'Authorization': authHeader } : {}) },
203203
body: JSON.stringify({ urls: [url] }),
204204
});
205205

@@ -270,7 +270,7 @@ async function handleSingleUrl(
270270

271271
const compressRes = await fetch(`${baseUrl}/api/compress`, {
272272
method: 'POST',
273-
headers: { 'Content-Type': 'application/json' },
273+
headers: { 'Content-Type': 'application/json', ...(authHeader ? { 'Authorization': authHeader } : {}) },
274274
body: JSON.stringify({
275275
content: content.text,
276276
url: content.url || url,
@@ -457,7 +457,7 @@ async function handleBatchUrls(
457457
// 批量获取内容
458458
const contentRes = await fetch(`${baseUrl}/api/content`, {
459459
method: 'POST',
460-
headers: { 'Content-Type': 'application/json' },
460+
headers: { 'Content-Type': 'application/json', ...(authHeader ? { 'Authorization': authHeader } : {}) },
461461
body: JSON.stringify({ urls: uncachedUrls }),
462462
});
463463

@@ -480,7 +480,7 @@ async function handleBatchUrls(
480480
try {
481481
const compressRes = await fetch(`${baseUrl}/api/compress`, {
482482
method: 'POST',
483-
headers: { 'Content-Type': 'application/json' },
483+
headers: { 'Content-Type': 'application/json', ...(authHeader ? { 'Authorization': authHeader } : {}) },
484484
body: JSON.stringify({
485485
content: content.text,
486486
url: content.url || url,
@@ -642,7 +642,7 @@ async function handleQuery(
642642
// Step 1: Exa 外部搜索
643643
const searchRes = await fetch(`${baseUrl}/api/search`, {
644644
method: 'POST',
645-
headers: { 'Content-Type': 'application/json' },
645+
headers: { 'Content-Type': 'application/json', ...(authHeader ? { 'Authorization': authHeader } : {}) },
646646
body: JSON.stringify({ query }),
647647
});
648648

@@ -723,7 +723,7 @@ async function handleQuery(
723723
try {
724724
const compressRes = await fetch(`${baseUrl}/api/compress`, {
725725
method: 'POST',
726-
headers: { 'Content-Type': 'application/json' },
726+
headers: { 'Content-Type': 'application/json', ...(authHeader ? { 'Authorization': authHeader } : {}) },
727727
body: JSON.stringify({
728728
content: result.text,
729729
url: result.url,

server/src/app/api/search/route.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NextRequest, NextResponse } from 'next/server';
22
import Exa from 'exa-js';
33
import { ensureNacosConfig } from '@/lib/nacos-config';
4+
import { apiGuard } from '@/lib/api-guard';
45
import { metrics } from '@/lib/metrics';
56

67
// Initialize Nacos config on module load (singleton pattern)
@@ -32,9 +33,12 @@ function getSearchApiKey(): string | undefined {
3233
export async function POST(request: NextRequest) {
3334
const reqStart = Date.now();
3435
try {
36+
const guard = await apiGuard(request, { tier: 'billable', estimatedCost: 1 });
37+
if (!guard.ok) return guard.response;
38+
3539
// Ensure Nacos config is loaded before accessing env vars
3640
await initNacos();
37-
41+
3842
const SEARCH_API_KEY = getSearchApiKey();
3943

4044
if (!SEARCH_API_KEY) {

0 commit comments

Comments
 (0)