mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
fix(server): bound every outbound fetch
The rule is a timeout, a size cap and boundary validation on anything that leaves the process, and several call sites had only the timeout. A link preview, a maps URL resolve, a wiki fallback and a holiday lookup all read the whole body into memory, so a caller who picks the URL picks how much memory the server spends - a few concurrent multi-hundred-megabyte responses is the whole heap. Two of them also fetched more than their name suggests. resolve-url fell through to a full page GET for any host once it found no coordinates, which makes the server a general-purpose outbound proxy wearing its own IP. The weather URLs took lat/lng straight from the caller into the query string. readCapped/exceedsDeclaredLength collect the pattern transit.service.ts and the plugin installer had each grown separately, so there is one of it now. Callers that only scan the head of a document (og: tags, a coordinate regex) keep the truncated bytes and find fewer fields; callers that must have the whole thing reject it.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { existsSync, promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { readEnv } from '../../app-config';
|
||||
import { exceedsDeclaredLength, readCapped, readCappedText } from '../../utils/cappedFetch';
|
||||
|
||||
/**
|
||||
* In-app Help/Wiki content, sourced from the `wiki/**` directory that ships with
|
||||
@@ -17,6 +18,12 @@ import { readEnv } from '../../app-config';
|
||||
const REPO = 'liketrek/TREK';
|
||||
const RAW_BASE = `https://raw.githubusercontent.com/${REPO}/main/wiki`;
|
||||
const TTL_MS = 60 * 60 * 1000; // remote fallback only: refresh from GitHub at most hourly
|
||||
// Remote fallback only: raw.githubusercontent.com is a third party on the
|
||||
// request path, so it gets a deadline and a size budget like every other
|
||||
// outbound client. A page or a screenshot over the budget falls through to the
|
||||
// stale-cache path instead of being buffered whole.
|
||||
const WIKI_TIMEOUT_MS = 8000;
|
||||
const WIKI_MAX_BYTES = 2 * 1024 * 1024;
|
||||
const SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
||||
|
||||
/**
|
||||
@@ -78,9 +85,11 @@ async function fetchText(file: string): Promise<string> {
|
||||
try {
|
||||
const res = await fetch(`${RAW_BASE}/${encodeURIComponent(file)}`, {
|
||||
headers: { 'User-Agent': 'TREK-help', Accept: 'text/plain' },
|
||||
signal: AbortSignal.timeout(WIKI_TIMEOUT_MS),
|
||||
});
|
||||
if (res.ok) {
|
||||
const text = await res.text();
|
||||
if (res.ok && !exceedsDeclaredLength(res, WIKI_MAX_BYTES)) {
|
||||
const { text, truncated } = await readCappedText(res, WIKI_MAX_BYTES);
|
||||
if (truncated) throw new Error('wiki page exceeds size limit');
|
||||
textCache.set(file, { data: text, ts: Date.now() });
|
||||
return text;
|
||||
}
|
||||
@@ -245,9 +254,11 @@ export async function getWikiAsset(assetPath: string): Promise<{ buf: Buffer; ty
|
||||
try {
|
||||
const res = await fetch(`${RAW_BASE}/${assetPath.split('/').map(encodeURIComponent).join('/')}`, {
|
||||
headers: { 'User-Agent': 'TREK-help' },
|
||||
signal: AbortSignal.timeout(WIKI_TIMEOUT_MS),
|
||||
});
|
||||
if (res.ok) {
|
||||
const buf = Buffer.from(await res.arrayBuffer());
|
||||
if (res.ok && !exceedsDeclaredLength(res, WIKI_MAX_BYTES)) {
|
||||
const { bytes: buf, truncated } = await readCapped(res, WIKI_MAX_BYTES);
|
||||
if (truncated) throw new Error('wiki asset exceeds size limit');
|
||||
assetCache.set(assetPath, { buf, type, ts: Date.now() });
|
||||
return { buf, type };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user