ai_api / src /lib /oauth /utils /codexAuthZipExtract.ts
Yogesh
initial deploy
cd8bd0a
Raw
History Blame Contribute Delete
2.61 kB
import path from "path";
import { unzipSync, type Unzipped } from "fflate";
export interface ExtractedZipFile {
name: string;
content: string;
}
export interface ExtractZipOptions {
maxFiles?: number;
maxFileSizeBytes?: number;
maxTotalSizeBytes?: number;
}
const DEFAULT_MAX_FILES = 50;
const DEFAULT_MAX_FILE_SIZE = 256 * 1024;
const DEFAULT_MAX_TOTAL = 10 * 1024 * 1024;
function isSafeEntryName(name: string): boolean {
if (!name.toLowerCase().endsWith(".json")) return false;
if (name.includes("..")) return false;
if (path.isAbsolute(name)) return false;
if (/[\r\n\0]/.test(name)) return false;
return true;
}
export function extractCodexAuthZip(
zipBuffer: Buffer,
options: ExtractZipOptions = {}
): ExtractedZipFile[] {
const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
const maxFileSize = options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE;
const maxTotal = options.maxTotalSizeBytes ?? DEFAULT_MAX_TOTAL;
let unzipped: Unzipped;
try {
unzipped = unzipSync(new Uint8Array(zipBuffer));
} catch {
throw new Error("Could not parse ZIP archive β€” file may be corrupt or not a valid ZIP");
}
const entries = Object.entries(unzipped).filter(([, data]) => data !== undefined);
const jsonEntries = entries.filter(([name]) => name.toLowerCase().endsWith(".json"));
if (jsonEntries.length === 0) {
throw new Error("ZIP archive contains no .json files");
}
if (jsonEntries.length > maxFiles) {
throw new Error(
`ZIP archive contains ${jsonEntries.length} .json files β€” max allowed is ${maxFiles}`
);
}
let totalBytes = 0;
const result: ExtractedZipFile[] = [];
for (const [entryName, data] of jsonEntries) {
const baseName = path.basename(entryName);
if (!isSafeEntryName(baseName)) {
throw new Error(
`ZIP entry "${baseName}" has an unsafe filename (must be a .json file without path traversal)`
);
}
if (!isSafeEntryName(entryName)) {
throw new Error(
`ZIP entry path "${entryName}" is unsafe (no "..", absolute paths, or control characters allowed)`
);
}
if (data.byteLength > maxFileSize) {
throw new Error(
`ZIP entry "${baseName}" is ${data.byteLength} bytes β€” exceeds ${maxFileSize} byte limit per file`
);
}
totalBytes += data.byteLength;
if (totalBytes > maxTotal) {
throw new Error(`ZIP archive total uncompressed size exceeds ${maxTotal} byte limit`);
}
const content = new TextDecoder("utf-8").decode(data);
result.push({ name: baseName, content });
}
return result;
}