api/ping: allowlist hosts and block redirects to close SSRF/LAN-scan vector
This commit is contained in:
parent
f5a913201a
commit
ea757595b0
3 changed files with 67 additions and 7 deletions
|
|
@ -1,5 +1,34 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
|
||||
// This route is reachable without authentication and it makes the server issue
|
||||
// an outbound request for whoever calls it. Without an allowlist,
|
||||
// `?url=http://192.168.x.x` turns the site into an open SSRF gadget / LAN port
|
||||
// scanner (and an open request proxy in general). Only the site's own services,
|
||||
// the Minecraft SMP and the friends' sites are actually health-checked.
|
||||
//
|
||||
// Host matching is exact or by subdomain (e.g. anything under
|
||||
// dvorakmortonxd.dev). Keep this in sync with the ServiceButton `url`/`checkUrl`
|
||||
// values in components/apps.tsx and components/TerminalLayout.tsx, and with the
|
||||
// friend list in data.ts.
|
||||
const ALLOWED_HOSTS = [
|
||||
'dvorakmortonxd.dev', // ai / jelly / navidrome / git / proxmox / mc (SMP)
|
||||
'skopec.dev',
|
||||
'bukovinafilip.com',
|
||||
'vlcak.me',
|
||||
];
|
||||
|
||||
function isAllowedHost(hostname: string): boolean {
|
||||
const host = hostname.toLowerCase().replace(/\.$/, '');
|
||||
return ALLOWED_HOSTS.some(
|
||||
(allowed) => host === allowed || host.endsWith(`.${allowed}`)
|
||||
);
|
||||
}
|
||||
|
||||
// A Minecraft address we are willing to forward to mcstatus.io: a plain
|
||||
// hostname/IPv4 with an optional port. Deliberately excludes '@', '/', '?', '#'
|
||||
// and IPv6 brackets so it cannot smuggle another path into the upstream URL.
|
||||
const MINECRAFT_ADDRESS = /^[a-z0-9.-]+(:\d{1,5})?$/i;
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const url = searchParams.get('url');
|
||||
|
|
@ -7,7 +36,12 @@ export async function GET(request: Request) {
|
|||
if (!url) return NextResponse.json({ status: 'down' });
|
||||
|
||||
if (url.startsWith('minecraft://')) {
|
||||
const address = url.replace('minecraft://', '');
|
||||
const address = url.slice('minecraft://'.length);
|
||||
const host = MINECRAFT_ADDRESS.test(address) ? address.split(':')[0] : '';
|
||||
if (!host || !isAllowedHost(host)) {
|
||||
return NextResponse.json({ status: 'down' });
|
||||
}
|
||||
|
||||
try {
|
||||
// mcsrvstat.us is unreliable at resolving some hostnames, so use mcstatus.io.
|
||||
const res = await fetch(`https://api.mcstatus.io/v2/status/java/${address}`, {
|
||||
|
|
@ -30,18 +64,38 @@ export async function GET(request: Request) {
|
|||
}
|
||||
}
|
||||
|
||||
let target: URL;
|
||||
try {
|
||||
const httpRes = await fetch(url, {
|
||||
target = new URL(url);
|
||||
} catch {
|
||||
return NextResponse.json({ status: 'down' });
|
||||
}
|
||||
|
||||
// Only allow-listed http(s) hosts. This rejects file:, ftp:, internal IPs,
|
||||
// `user@host` tricks and anything the allowlist does not cover.
|
||||
if (
|
||||
(target.protocol !== 'http:' && target.protocol !== 'https:') ||
|
||||
!isAllowedHost(target.hostname)
|
||||
) {
|
||||
return NextResponse.json({ status: 'down' });
|
||||
}
|
||||
|
||||
try {
|
||||
const httpRes = await fetch(target, {
|
||||
method: 'GET',
|
||||
signal: AbortSignal.timeout(5000)
|
||||
// Never follow redirects: an allowed host could otherwise redirect to an
|
||||
// internal address and reopen the SSRF. A 3xx still means the service
|
||||
// answered, so it counts as "up".
|
||||
redirect: 'manual',
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
|
||||
// Accept all non-5xx responses as "up". 5xx usually means origin or gateway errors (like Cloudflare 521, 522, etc.)
|
||||
if (httpRes && httpRes.status < 500) {
|
||||
return NextResponse.json({ status: 'up' });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to ping ${url}`, e);
|
||||
} catch {
|
||||
// Request failed or timed out; fall through to "down".
|
||||
}
|
||||
|
||||
return NextResponse.json({ status: 'down' });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue