import crypto from "node:crypto"; import { createWriteStream } from "node:fs"; import fs from "node:fs/promises"; import { request as httpRequest } from "node:http"; import { request as httpsRequest } from "node:https"; import path from "node:path"; import { pipeline } from "node:stream/promises"; import { retainSafeHeadersForCrossOriginRedirect } from "../infra/net/redirect-headers.js"; import { resolvePinnedHostname } from "../infra/net/ssrf.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; import { resolveConfigDir } from "../utils.js"; import { detectMime, extensionForMime } from "./mime.js"; import { isSafeOpenError, readLocalFileSafely, type SafeOpenLikeError } from "./store.runtime.js";
const resolveMediaDir = () => path.join(resolveConfigDir(), "media");
export const MEDIA_MAX_BYTES = 5 * 1024 * 1024; // 5MB default const MAX_BYTES = MEDIA_MAX_BYTES; const DEFAULT_TTL_MS = 2 * 60 * 1000; // 2 minutes // Files are intentionally readable by non-owner UIDs so Docker sandbox containers can access // inbound media. The containing state/media directories remain 0o700, which is the trust boundary. const MEDIA_FILE_MODE = 0o644;
type CleanOldMediaOptions = {
recursive?: boolean;
pruneEmptyDirs?: boolean;
};
type RequestImpl = typeof httpRequest;
type ResolvePinnedHostnameImpl = typeof resolvePinnedHostname;
let httpRequestImpl: RequestImpl = defaultHttpRequestImpl;
let httpsRequestImpl: RequestImpl = defaultHttpsRequestImpl;
let resolvePinnedHostnameImpl: ResolvePinnedHostnameImpl = defaultResolvePinnedHostnameImpl;
/** *ResolvesamediaIDsavedbysaveMediaBuffertoitsabsolutephysicalpath. * *Thisistheread-sidecounterparttosaveMediaBufferandisusedbythe *agentrunnertohydrateopaque`media://inbound/<id>` URIs written by the *Gateway'sclaim-checkoffloadpath. * *Security: *-RejectsIDscontainingpathseparators,"..",ornullbytestoprevent *directorytraversalandpathinjectionoutsidetheresolvedsubdir. *-Verifiestheresolvedpathisaregularfile(notasymlinkordirectory) *beforereturningit,matchingthewrite-sideMEDIA_FILE_MODEpolicy. * *@paramidThemediaIDasreturnedbySavedMedia.id(mayinclude *extensionandoriginal-filenameprefix, *e.g."photo---<uuid>.png"or"图片---<uuid>.png"). *@paramsubdirThesubdirectorythefilewassavedinto(default"inbound"). *@returnsAbsolutepathtothefileondisk. *@throwsIftheIDisunsafe,thefiledoesnotexist,orisnota *regularfile.
*/
export async function resolveMediaBufferPath(
id: string,
subdir: "inbound" = "inbound",
): Promise<string> { // Guard against path traversal and null-byte injection. // // - Separator checks: reject any ID containing "/" or "\" (covers all // relative traversal sequences such as "../foo" or "..\\foo"). // - Exact ".." check: reject the bare traversal operator in case a caller // strips separators but keeps the dots. // - Null-byte check: reject "\0" which can truncate paths on some platforms // and cause the OS to open a different file than intended. // // We allow consecutive dots in legitimate filenames (e.g. "report..draft.png"), // so we only reject the exact two-character string "..". // // JSON.stringify is used in the error message so that control characters // (including \0) are rendered visibly in logs rather than silently dropped. if (!id || id.includes("/") || id.includes("\\") || id.includes("\0") || id === "..") { thrownew Error(`resolveMediaBufferPath: unsafe media ID: ${JSON.stringify(id)}`);
}
const dir = path.join(resolveMediaDir(), subdir); const resolved = path.join(dir, id);
// Double-check that path.join didn't escape the intended directory. // This should be unreachable after the separator check above, but be // explicit about the invariant. if (!resolved.startsWith(dir + path.sep) && resolved !== dir) { thrownew Error(`resolveMediaBufferPath: path escapes media directory: ${JSON.stringify(id)}`);
}
// lstat (not stat) so we see symlinks rather than following them. const stat = await fs.lstat(resolved);
if (stat.isSymbolicLink()) { thrownew Error(
`resolveMediaBufferPath: refusing to follow symlink for media ID: ${JSON.stringify(id)}`,
);
} if (!stat.isFile()) { thrownew Error(
`resolveMediaBufferPath: media ID does not resolve to a file: ${JSON.stringify(id)}`,
);
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.