mirror of
https://github.com/jason5ng32/MyIP.git
synced 2026-11-04 08:55:47 +00:00
Express 5 exposes req.query through a getter that re-parses the URL on every read, so the guards' in-place normalization was silently dropped: ?type=caa reached dns-resolver lowercase, ?asn=AS13335 reached the ASN handlers unparsed, ?q=as13335 missed whois's AS<n> branch. Guards now pin the parsed query as an own property before rewriting it, and a regression block exercises them on a real Express request. Fixes #521 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
177 lines
7.4 KiB
JavaScript
177 lines
7.4 KiB
JavaScript
// Express middleware that factors out the boilerplate every api/ handler
|
|
// used to repeat at the top of its function body: referer check, param
|
|
// presence check, param validity check. Mount once in backend-server.js;
|
|
// handlers stop carrying the defensive checks and can't accidentally forget
|
|
// them.
|
|
|
|
import { refererCheck } from './referer-check.js';
|
|
import { isValidIP, isValidDomain, isUsablePublicIP } from '../common/valid-ip.js';
|
|
import { isValidBgpPrefix } from '../common/bgp-prefix.js';
|
|
import { STATUS_PROVIDER_IDS } from './service-status-providers.js';
|
|
import { DNS_RECORD_TYPE_SET } from '../common/dns-record-types.js';
|
|
import { parseAsnInput } from '../common/asn-input.js';
|
|
|
|
// Express 5's req.query is a prototype getter that re-parses the URL on every
|
|
// read, so a direct write is lost. Pin the parsed object as an own property
|
|
// first; later guards, cacheable resolvers and the handler then share it.
|
|
const setQueryParam = (req, name, value) => {
|
|
if (!Object.hasOwn(req, 'query')) {
|
|
Object.defineProperty(req, 'query', {
|
|
value: req.query, writable: true, configurable: true, enumerable: true,
|
|
});
|
|
}
|
|
req.query[name] = value;
|
|
};
|
|
|
|
// Reject requests without an allowed referer. The error message variant
|
|
// preserves the existing user-facing wording.
|
|
export const requireReferer = (req, res, next) => {
|
|
const referer = req.headers.referer;
|
|
if (!refererCheck(referer)) {
|
|
return res.status(403).json({
|
|
error: referer ? 'Access denied' : 'What are you doing?',
|
|
});
|
|
}
|
|
next();
|
|
};
|
|
|
|
// Reject requests whose IP query param isn't a publicly routable address.
|
|
// Every `?ip=` route asks an external service about that address — a
|
|
// registry, a geolocation source — and reserved space has no answer there,
|
|
// so it never reaches an upstream call. Malformed input and reserved space
|
|
// get distinct messages; the two failures need different fixes.
|
|
// Factory so a route using another param name (none today, but leaving the
|
|
// door open) can say `requirePublicIP('target')`.
|
|
export const requirePublicIP = (paramName = 'ip') => (req, res, next) => {
|
|
const ip = req.query[paramName];
|
|
if (!ip) {
|
|
return res.status(400).json({ error: 'No IP address provided' });
|
|
}
|
|
if (!isValidIP(ip)) {
|
|
return res.status(400).json({ error: 'Invalid IP address' });
|
|
}
|
|
if (!isUsablePublicIP(ip)) {
|
|
return res.status(400).json({ error: 'Not a public IP address' });
|
|
}
|
|
next();
|
|
};
|
|
|
|
// Reject requests without a syntactically valid domain in the specified
|
|
// query param. Lowercases the value in place so handlers and the edge cache
|
|
// see one canonical form (domains are case-insensitive; a mixed-case query
|
|
// must not become a distinct CF cache entry).
|
|
export const requireValidDomain = (paramName = 'domain') => (req, res, next) => {
|
|
const raw = req.query[paramName];
|
|
if (!raw) {
|
|
return res.status(400).json({ error: 'No domain provided' });
|
|
}
|
|
const domain = String(raw).toLowerCase();
|
|
if (!isValidDomain(domain)) {
|
|
return res.status(400).json({ error: 'Invalid domain' });
|
|
}
|
|
setQueryParam(req, paramName, domain);
|
|
next();
|
|
};
|
|
|
|
// Reject requests without a valid CIDR prefix in the specified query param.
|
|
// Accepts any well-formed CIDR — the quantization policy (e.g. /24 for v4,
|
|
// /48 for v6) is the frontend's job, not the guard's.
|
|
export const requireValidPrefix = (paramName = 'prefix') => (req, res, next) => {
|
|
const prefix = req.query[paramName];
|
|
if (!prefix) {
|
|
return res.status(400).json({ error: 'No prefix provided' });
|
|
}
|
|
if (!isValidBgpPrefix(prefix)) {
|
|
return res.status(400).json({ error: 'Invalid prefix' });
|
|
}
|
|
next();
|
|
};
|
|
|
|
// Reject requests without a valid ASN (AS1 … AS4294967295, up to ten
|
|
// digits, optional 'AS' prefix — parseAsnInput in common/asn-input.js) and
|
|
// rewrite it in place to the canonical number string (`AS013335` → `13335`),
|
|
// so the edge cache sees one key. Used by /api/asn-connectivity,
|
|
// /api/asn-profile and, per-view, the /api/cfradar dispatcher.
|
|
export const requireValidASN = (paramName = 'asn') => (req, res, next) => {
|
|
const raw = req.query[paramName];
|
|
if (!raw) {
|
|
return res.status(400).json({ error: 'No ASN provided' });
|
|
}
|
|
const asn = parseAsnInput(raw);
|
|
if (asn === null) {
|
|
return res.status(400).json({ error: 'Invalid ASN' });
|
|
}
|
|
setQueryParam(req, paramName, String(asn));
|
|
next();
|
|
};
|
|
|
|
// Canonicalize an ASN-shaped value (`AS13335`, `as13335` — the `AS` prefix
|
|
// is required) to `AS<n>` in place, rejecting AS0 and anything past 32 bits.
|
|
// Any other value, a bare number included, passes through untouched —
|
|
// /api/whois also takes IPs and domains, which its handler validates.
|
|
export const normalizeAsnQuery = (paramName = 'q') => (req, res, next) => {
|
|
const raw = req.query[paramName];
|
|
if (typeof raw !== 'string' || !/^AS\d+$/i.test(raw.trim())) return next();
|
|
const asn = parseAsnInput(raw, { requirePrefix: true });
|
|
if (asn === null) {
|
|
return res.status(400).json({ error: 'Invalid ASN' });
|
|
}
|
|
setQueryParam(req, paramName, `AS${asn}`);
|
|
next();
|
|
};
|
|
|
|
// Reject requests without a two-letter country code; uppercases in place so
|
|
// the edge cache sees one canonical key. Syntactic only — an unassigned code
|
|
// just yields an empty upstream series.
|
|
export const requireValidCountry = (paramName = 'country') => (req, res, next) => {
|
|
const raw = req.query[paramName];
|
|
if (!raw) {
|
|
return res.status(400).json({ error: 'No country provided' });
|
|
}
|
|
if (!/^[A-Za-z]{2}$/.test(raw)) {
|
|
return res.status(400).json({ error: 'Invalid country' });
|
|
}
|
|
setQueryParam(req, paramName, String(raw).toUpperCase());
|
|
next();
|
|
};
|
|
|
|
// Reject requests whose report id doesn't look like one we issued: 16 random
|
|
// bytes as base64url, always exactly 22 chars. Reads the ROUTE param (not the
|
|
// query) — the share-report read endpoint is /api/report/:id.
|
|
export const requireValidReportId = (paramName = 'id') => (req, res, next) => {
|
|
const id = req.params?.[paramName];
|
|
if (!id || !/^[A-Za-z0-9_-]{22}$/.test(id)) {
|
|
return res.status(400).json({ error: 'Invalid report id' });
|
|
}
|
|
next();
|
|
};
|
|
|
|
// Whitelist ?type= against the record types the resolver actually handles.
|
|
// Without it the DoH branch forwards any string verbatim to four third-party
|
|
// endpoints, which makes this route a query proxy for types we never support.
|
|
export const requireValidRecordType = (paramName = 'type') => (req, res, next) => {
|
|
const type = req.query[paramName];
|
|
if (!type) {
|
|
return res.status(400).json({ error: 'No record type provided' });
|
|
}
|
|
if (!DNS_RECORD_TYPE_SET.has(String(type).toUpperCase())) {
|
|
return res.status(400).json({ error: 'Invalid record type' });
|
|
}
|
|
setQueryParam(req, paramName, String(type).toUpperCase());
|
|
next();
|
|
};
|
|
|
|
// Reject requests whose `id` isn't a known service-status provider slug.
|
|
// Used by the per-provider components / incidents endpoints, which select a
|
|
// row from the in-memory snapshot by id.
|
|
export const requireValidProviderId = (paramName = 'id') => (req, res, next) => {
|
|
const id = req.query[paramName];
|
|
if (!id) {
|
|
return res.status(400).json({ error: 'No provider id provided' });
|
|
}
|
|
if (!STATUS_PROVIDER_IDS.has(id)) {
|
|
return res.status(400).json({ error: 'Invalid provider id' });
|
|
}
|
|
next();
|
|
};
|