fix(security): limit skill URL import reads
This commit is contained in:
1 parent
0cbaabf937
commit
f5b3de438e
5 files changed
+238
-39
No files matched your search
@@ -3,7 +3,9 @@ import type { AppDatabase } from "../database";
|
||||
import type { FileInventoryEntry, SkillImportResult } from "../skill-types.js";
|
||||
import { parseFrontmatterMarkdown } from "../skill-format.js";
|
||||
import { deriveCanonicalSkillKey, normalizeSkillSlug } from "../skill-keys.js";
|
||||
import { MAX_SKILL_BYTES } from "../skill-github.js";
|
||||
import { materializeSkillFiles } from "./materialize";
|
||||
import { readResponseWithLimit } from "./read-response-with-limit.js";
|
||||
|
||||
export interface UrlImportContext {
|
||||
db: AppDatabase;
|
||||
@@ -26,7 +28,7 @@ export async function importFromUrl(
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch skill from URL: ${response.status} ${url}`);
|
||||
}
|
||||
const markdown = await response.text();
|
||||
const markdown = await readResponseWithLimit(response, MAX_SKILL_BYTES);
|
||||
const parsed = parseFrontmatterMarkdown(markdown);
|
||||
const nowTs = new Date().toISOString();
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
export async function readResponseWithLimit(response: Response, maxBytes: number): Promise<string> {
|
||||
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
|
||||
throw new Error(`Invalid response byte limit: ${maxBytes}`);
|
||||
}
|
||||
|
||||
const contentLengthHeader = response.headers.get("Content-Length");
|
||||
if (contentLengthHeader !== null) {
|
||||
const contentLength = Number(contentLengthHeader);
|
||||
if (!Number.isSafeInteger(contentLength) || contentLength < 0) {
|
||||
throw new Error(`Invalid Content-Length header: ${contentLengthHeader}`);
|
||||
}
|
||||
if (contentLength > maxBytes) {
|
||||
throw new Error(`Response too large: ${contentLength} bytes (max ${maxBytes})`);
|
||||
}
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
const text = await response.text();
|
||||
const byteLength = new TextEncoder().encode(text).byteLength;
|
||||
if (byteLength > maxBytes) {
|
||||
throw new Error(`Response body exceeded ${maxBytes} bytes`);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
const chunks: Uint8Array[] = [];
|
||||
let totalBytes = 0;
|
||||
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
totalBytes += value.byteLength;
|
||||
if (totalBytes > maxBytes) {
|
||||
await reader.cancel();
|
||||
throw new Error(`Response body exceeded ${maxBytes} bytes`);
|
||||
}
|
||||
chunks.push(value);
|
||||
}
|
||||
|
||||
const body = new Uint8Array(totalBytes);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
body.set(chunk, offset);
|
||||
offset += chunk.byteLength;
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(body);
|
||||
}
|
||||
Reference in new issue
Block a user