fix: validate HTTP dates IPv6 URLs and parser errors
CSS quality / css-quality (push) Has been cancelled

This commit is contained in:
hectorzhao
2026-09-14 15:15:58 +08:00
parent 92b112cc6e
commit a420d61b23
16 changed files with 403 additions and 58 deletions
+21 -7
View File
@@ -26,7 +26,7 @@ import { SendChainService } from '../send-chain/send-chain.service';
import { decryptSecret, encryptSecret } from './open-api.crypto';
import type { OpenApiAuthContext } from './open-api.types';
import { ProtocolLogsService } from '../protocol-logs/protocol-logs.service';
import { pinnedWebhookLookup, publicOpenApiFailure, webhookJobId } from './open-api.protocol';
import { parseOpenApiDate, pinnedWebhookLookup, publicOpenApiFailure, webhookJobId } from './open-api.protocol';
import { automaticDeliveryMode } from './delivery-mode';
export const OPEN_API_WEBHOOK_TRANSPORT = Symbol('open-api-webhook-transport');
@@ -449,8 +449,9 @@ export class OpenApiService implements OnModuleInit, OnModuleDestroy {
if (value !== undefined && typeof value !== 'string')
throw new BadRequestException({ code: 'PARAMETER_INVALID', message: '查询参数必须为单个字符串' });
}
const endTime = query.endTime ? new Date(query.endTime) : new Date();
const startTime = query.startTime ? new Date(query.startTime) : new Date(endTime.getTime() - 24 * 3600_000);
const endTime = query.endTime !== undefined ? parseOpenApiDate(query.endTime) : new Date();
const startTime =
query.startTime !== undefined ? parseOpenApiDate(query.startTime) : new Date(endTime.getTime() - 24 * 3600_000);
if (!Number.isFinite(startTime.getTime()) || !Number.isFinite(endTime.getTime()) || startTime > endTime)
throw new BadRequestException({ code: 'TIME_RANGE_INVALID', message: '查询时间范围非法' });
if (endTime.getTime() - startTime.getTime() > auth.config.maxQueryRangeDays * 86400_000)
@@ -901,7 +902,7 @@ async function validateWebhookUrl(value: string, requireHttps: boolean) {
return (await resolveWebhookTarget(value, requireHttps)).url.toString();
}
async function resolveWebhookTarget(value: string, requireHttps: boolean) {
export async function resolveWebhookTarget(value: string, requireHttps: boolean) {
let url: URL;
try {
url = new URL(String(value ?? '').trim());
@@ -911,7 +912,13 @@ async function resolveWebhookTarget(value: string, requireHttps: boolean) {
if (!['http:', 'https:'].includes(url.protocol)) throw new BadRequestException('Webhook仅支持HTTP/HTTPS');
if (requireHttps && url.protocol !== 'https:') throw new BadRequestException('当前应用要求Webhook使用HTTPS');
if (url.username || url.password) throw new BadRequestException('Webhook URL不能包含用户名或密码');
const addresses = isIP(url.hostname) ? [{ address: url.hostname }] : await lookup(url.hostname, { all: true });
const hostname = url.hostname.startsWith('[') ? url.hostname.slice(1, -1) : url.hostname;
let addresses: Array<{ address: string }>;
try {
addresses = isIP(hostname) ? [{ address: hostname }] : await lookup(hostname, { all: true });
} catch {
throw new BadRequestException('Webhook域名未解析到可用地址');
}
if (addresses.some(({ address }) => isPrivateAddress(address)))
throw new BadRequestException('Webhook URL不能指向内网、环回或链路本地地址');
const selected = addresses[0];
@@ -958,7 +965,14 @@ async function postWebhook(
}
function isPrivateAddress(address: string) {
const normalized = address.replace(/^::ffff:/, '');
const canonical = isIP(address) === 6 ? new URL(`http://[${address}]`).hostname.slice(1, -1) : address;
const mapped = /^::ffff:([a-f0-9]{1,4}):([a-f0-9]{1,4})$/i.exec(canonical);
if (mapped) {
const high = parseInt(mapped[1], 16),
low = parseInt(mapped[2], 16);
return isPrivateAddress(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
}
const normalized = canonical.toLowerCase();
if (
normalized === '::1' ||
normalized === '::' ||
@@ -994,7 +1008,7 @@ function decodeCursor(value?: string) {
if (!Array.isArray(parsed) || parsed.length !== 2 || typeof parsed[0] !== 'string' || typeof parsed[1] !== 'string')
throw new Error();
const [date, id] = parsed;
const receivedAt = new Date(date);
const receivedAt = parseOpenApiDate(date);
if (!id || !Number.isFinite(receivedAt.getTime())) throw new Error();
return { receivedAt, id };
} catch {