Release v1.4.26 with remaining bug audit fixes

- AllDebrid: add HTML response detection to unrestrictLink
- Cleanup: skip symlinks/junctions in all directory traversals
- Blob URL: increase revoke delay from 0ms to 60s
- Extractor: per-package progress file to prevent collision
- ADD_CONTAINERS: reject path traversal and relative paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe
2026-02-28 13:09:59 +01:00
co-authored by Claude Opus 4.6
parent 06a272ccbd
commit cbc423e4b7
7 changed files with 200 additions and 92 deletions
+58 -18
View File
@@ -444,27 +444,61 @@ class AllDebridClient {
body.append("link[]", link);
}
const response = await fetch(`${ALL_DEBRID_API_BASE}/link/infos`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.token}`,
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "RD-Node-Downloader/1.1.15"
},
body,
signal: AbortSignal.timeout(API_TIMEOUT_MS)
});
let payload: Record<string, unknown> | null = null;
let chunkResolved = false;
for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) {
let response: Response;
let text = "";
try {
response = await fetch(`${ALL_DEBRID_API_BASE}/link/infos`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.token}`,
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "RD-Node-Downloader/1.1.15"
},
body,
signal: AbortSignal.timeout(API_TIMEOUT_MS)
});
const text = await response.text();
const payload = asRecord(parseJson(text));
if (!response.ok) {
throw new Error(parseError(response.status, text, payload));
text = await response.text();
payload = asRecord(parseJson(text));
if (!response.ok) {
const reason = parseError(response.status, text, payload);
if (shouldRetryStatus(response.status) && attempt < REQUEST_RETRIES) {
await sleep(retryDelay(attempt));
continue;
}
throw new Error(reason);
}
const contentType = String(response.headers.get("content-type") || "").toLowerCase();
const looksHtml = contentType.includes("text/html") || /^\s*<(!doctype\s+html|html\b)/i.test(text);
if (looksHtml) {
throw new Error("AllDebrid lieferte HTML statt JSON");
}
if (!payload) {
throw new Error("AllDebrid Antwort ist kein JSON-Objekt");
}
const status = pickString(payload, ["status"]);
if (status && status.toLowerCase() === "error") {
const errorObj = asRecord(payload?.error);
throw new Error(pickString(errorObj, ["message", "code"]) || "AllDebrid API error");
}
chunkResolved = true;
break;
} catch (error) {
if (attempt >= REQUEST_RETRIES) {
throw error;
}
await sleep(retryDelay(attempt));
}
}
const status = pickString(payload, ["status"]);
if (status && status.toLowerCase() === "error") {
const errorObj = asRecord(payload?.error);
throw new Error(pickString(errorObj, ["message", "code"]) || "AllDebrid API error");
if (!chunkResolved || !payload) {
throw new Error("AllDebrid Link-Infos konnten nicht geladen werden");
}
const data = asRecord(payload?.data);
@@ -519,6 +553,12 @@ class AllDebridClient {
throw new Error(reason);
}
const contentType = String(response.headers.get("content-type") || "").toLowerCase();
const looksHtml = contentType.includes("text/html") || /^\s*<(!doctype\s+html|html\b)/i.test(text);
if (looksHtml) {
throw new Error("AllDebrid lieferte HTML statt JSON");
}
const status = pickString(payload, ["status"]);
if (status && status.toLowerCase() === "error") {
const errorObj = asRecord(payload?.error);