69 lines
2.2 KiB
JavaScript
69 lines
2.2 KiB
JavaScript
// File helpers: content hashing, relative-path normalization, reading bytes.
|
|
|
|
const crypto = require("crypto");
|
|
const fs = require("fs");
|
|
|
|
|
|
// SHA-256 of the file's contents, returned as lowercase hex.
|
|
const sha256File = async (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);
|
|
});
|
|
};
|
|
|
|
|
|
const sha256Buffer = (buf) => {
|
|
return crypto.createHash("sha256").update(buf).digest("hex");
|
|
};
|
|
|
|
|
|
// Convert Saltcorn's absolute file location to a tenant-relative serve path,
|
|
// using File.absPathToServePath. The relative path is what we transport
|
|
// between instances (each has a different file_store root).
|
|
const toRelativePath = (File, absPath) => {
|
|
if (!absPath) return "";
|
|
return File.absPathToServePath(absPath);
|
|
};
|
|
|
|
|
|
// Convert a relative serve path back to the absolute path on this instance.
|
|
// SECURITY: relPath arrives 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 write/read arbitrary files (e.g. /etc/cron.d, app JS) -> RCE.
|
|
const toAbsolutePath = (File, db, relPath) => {
|
|
const path = require("path");
|
|
const tenant = db.getTenantSchema();
|
|
const root = path.resolve(db.connectObj.file_store, tenant);
|
|
const abs = path.resolve(root, String(relPath || ""));
|
|
if (abs !== root && !abs.startsWith(root + path.sep)) {
|
|
throw new Error(`refusing file path outside tenant store: ${relPath}`);
|
|
}
|
|
return abs;
|
|
};
|
|
|
|
|
|
const readFileBytes = async (absPath) => {
|
|
return await fs.promises.readFile(absPath);
|
|
};
|
|
|
|
|
|
const writeFileBytes = async (absPath, buf) => {
|
|
const path = require("path");
|
|
await fs.promises.mkdir(path.dirname(absPath), { recursive: true });
|
|
await fs.promises.writeFile(absPath, buf);
|
|
};
|
|
|
|
|
|
module.exports = {
|
|
sha256File,
|
|
sha256Buffer,
|
|
toRelativePath,
|
|
toAbsolutePath,
|
|
readFileBytes,
|
|
writeFileBytes
|
|
};
|