Files
readest/apps/readest-app/src/pages/api/kosync.ts
T
Huang Xin 184de9210d fix(security): prevent SSRF in kosync proxy (CWE-918) (#3793)
Validate serverUrl in the kosync API proxy to block requests to
private/internal addresses and non-http(s) schemes. Also fix isLanAddress
to detect 0.0.0.0 and bracket-wrapped IPv6 private addresses.

Closes code-scanning alert #14.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 21:21:51 +02:00

72 lines
2.1 KiB
TypeScript

import type { NextApiRequest, NextApiResponse } from 'next';
import { corsAllMethods, runMiddleware } from '@/utils/cors';
import { isLanAddress } from '@/utils/network';
import { KoSyncProxyPayload } from '@/types/kosync';
const validEndpoints = [/\/users\/create/, /\/users\/auth/, /\/syncs\/progress/];
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
await runMiddleware(req, res, corsAllMethods);
const {
serverUrl,
endpoint,
method,
headers: clientHeaders,
body: clientBody,
} = req.body as KoSyncProxyPayload;
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method Not Allowed' });
}
if (!serverUrl || !endpoint) {
return res.status(400).json({ error: 'serverUrl and endpoint are required' });
}
if (!validEndpoints.some((regex) => regex.test(endpoint))) {
return res.status(400).json({ error: 'Invalid endpoint' });
}
try {
const parsed = new URL(serverUrl);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return res.status(400).json({ error: 'Only http and https URLs are allowed' });
}
} catch {
return res.status(400).json({ error: 'Invalid serverUrl' });
}
if (isLanAddress(serverUrl)) {
return res
.status(400)
.json({ error: 'Requests to private/internal addresses are not allowed' });
}
const targetUrl = `${serverUrl.replace(/\/$/, '')}${endpoint}`;
try {
const response = await fetch(targetUrl, {
method: method,
headers: {
...clientHeaders,
Accept: 'application/vnd.koreader.v1+json',
'Content-Type': 'application/json',
},
body: clientBody ? JSON.stringify(clientBody) : null,
});
const data = await response.text();
res.status(response.status);
try {
res.json(JSON.parse(data));
} catch {
res.send(data);
}
} catch (error) {
console.error('[KOSYNC PROXY] Error:', error);
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
res.status(500).json({ error: 'Proxy request failed', details: errorMessage });
}
}