fix(gbfs): adapt redirect hardening to provider modules and bound body reads

This commit is contained in:
Sameh Khamis committed 2026-09-12 15:13:14 -07:00
commit 4fae7fb278
6 files changed
+435 -77

No files matched your search

+30 -25
View File
@@ -3,44 +3,49 @@
* oversized Content-Length, then streams with a running cap so a chunked or
* length-omitted response cannot blow past the limit. Throws { code:'RESPONSE_TOO_LARGE' }.
*/
export async function readResponseTextCapped(response, maxBytes) {
export async function readResponseTextCapped(response, maxBytes, signal) {
const tooLarge = () =>
Object.assign(new Error('Upstream response too large'), {
code: 'RESPONSE_TOO_LARGE',
});
const declared = Number(response.headers.get('content-length'));
if (Number.isFinite(declared) && declared > maxBytes) {
const err = new Error('Upstream response too large');
err.code = 'RESPONSE_TOO_LARGE';
throw err;
void response.body?.cancel().catch(() => {});
throw tooLarge();
}
signal?.throwIfAborted();
const reader = response.body?.getReader?.();
if (!reader) {
const text = await response.text();
if (Buffer.byteLength(text) > maxBytes) {
const err = new Error('Upstream response too large');
err.code = 'RESPONSE_TOO_LARGE';
throw err;
}
signal?.throwIfAborted();
if (Buffer.byteLength(text) > maxBytes) throw tooLarge();
return text;
}
const cancel = () => {
void reader.cancel().catch(() => {});
};
signal?.addEventListener('abort', cancel, { once: true });
const decoder = new TextDecoder();
let out = '';
let total = 0;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
total += value.byteLength;
if (total > maxBytes) {
try {
await reader.cancel();
} catch {
/* no-op */
}
const err = new Error('Upstream response too large');
err.code = 'RESPONSE_TOO_LARGE';
throw err;
try {
for (;;) {
signal?.throwIfAborted();
const { done, value } = await reader.read();
signal?.throwIfAborted();
if (done) break;
total += value.byteLength;
if (total > maxBytes) throw tooLarge();
out += decoder.decode(value, { stream: true });
}
out += decoder.decode(value, { stream: true });
return out + decoder.decode();
} catch (error) {
cancel();
throw error;
} finally {
signal?.removeEventListener('abort', cancel);
reader.releaseLock();
}
out += decoder.decode();
return out;
}
/** Parse a fetch() JSON response only after enforcing a hard byte cap. */