sc-site-sync/lib/common/fileStore.js
2026-10-19 01:25:16 -05:00

52 lines
1.8 KiB
JavaScript

// 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
};