// File helpers: content hashing and tenant-store // path containment. const crypto = require("crypto"); const fs = require("fs"); const path = require("path"); const { sha256Hex } = require("./hash"); // SHA-256 of a file's contents as lowercase hex, streamed (constant memory). const sha256File = (absPath) => { return new Promise((resolve, reject) => { const hash = crypto.createHash("sha256"); const stream = fs.createReadStream(absPath); stream.on("data", (chunk) => hash.update(chunk)); stream.on("end", () => resolve(hash.digest("hex"))); stream.on("error", reject); }); }; // Content hash of a Saltcorn File model instance, streamed off disk. S3-backed // files have no local path, so fall back to a buffered download there. const hashFileContents = async (file) => { if (file.s3_store) { return sha256Hex(await file.get_contents()); } return await sha256File(file.location); }; // Absolute path on this instance of a tenant-relative file path. // SECURITY: the value can originate from a (HMAC-authenticated) peer payload, so // it is attacker-influenceable. Resolve it and assert it stays strictly within // this tenant's file_store root -- otherwise a "../../.." (or absolute) path // would let a peer read/write arbitrary files (e.g. /etc/cron.d, app JS -> RCE) // or another tenant's. The root itself is not a file. const toAbsolutePath = (db, relPath) => { const root = path.resolve(db.connectObj.file_store, db.getTenantSchema()); const abs = path.resolve(root, String(relPath || "")); if (!abs.startsWith(root + path.sep)) { throw new Error(`refusing file path outside tenant store: ${relPath}`); } return abs; }; module.exports = { hashFileContents, toAbsolutePath };