52 lines
1.8 KiB
JavaScript
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
|
|
};
|