UNPKG

@haxtheweb/create

Version:

CLI for all things HAX the web

62 lines (56 loc) 3.62 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.guardRecipeTokens = guardRecipeTokens; exports.isSSRFError = isSSRFError; exports.resolveLocalPath = resolveLocalPath; exports.sanitizeIfString = sanitizeIfString; var path = _interopRequireWildcard(require("node:path")); var sanitizeContentLib = _interopRequireWildcard(require("@haxtheweb/haxcms-nodejs/dist/lib/sanitizeContent.js")); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } // Security helpers extracted from programs/site.js so they are unit-testable // without loading the full site.js (which top-level imports haxcms-nodejs dist // modules that may not yet ship in the published package). site.js imports these // back. Only touches node:path and the PRESENT sanitizeContent.js dist module. // // NOTE: the RECIPE_TOKEN_DENY char class is built with new RegExp + String.fromCharCode // (not a /\n\r/ literal) so the file has no backslash escapes and the LF/CR members // are unambiguous. The matched set is identical to the original site.js regex. const sanitizeHTMLForStorage = sanitizeContentLib.sanitizeHTMLForStorage; // Security (H-4): recipe files can contain arbitrary text that used to be // passed straight to exec() as a shell string. Replaying a recipe now invokes // the CLI via spawn() with an argument array (no shell) so recipe contents // cannot inject shell commands. Tokens are also guarded so malformed recipes // fail loudly instead of producing surprising argv. const RECIPE_TOKEN_DENY = new RegExp('[;&|`$<>(){}!' + String.fromCharCode(10) + String.fromCharCode(13) + ']'); function guardRecipeTokens(tokens) { for (const t of tokens) { if (typeof t !== 'string' || RECIPE_TOKEN_DENY.test(t)) { throw new Error(`Recipe token rejected (contains shell metacharacters): ${t}`); } } return tokens; } // Security (H-1/H-2/H-3): true when an error thrown by safeFetch/ // assertUrlNotSSRF is an SSRF rejection (stable .code prefix) rather than a // generic network error, so callers can surface a clear message. function isSSRFError(e) { return Boolean(e && typeof e.code === 'string' && e.code.startsWith('SSRF_')); } // Security (H-5): sanitize remote-derived HTML before it is written into page // content. Non-string values (e.g. parsed JSON/YAML objects from --format) and // empty strings pass through unchanged so non-HTML import formats are unaffected. function sanitizeIfString(html) { return typeof html === 'string' && html.length > 0 ? sanitizeHTMLForStorage(html) : html; } // Security (L-1): canonicalize a local filesystem path and reject null bytes // (a classic fs-path-injection vector). No fixed base is enforced because these // options legitimately point anywhere on the user's filesystem; path.resolve is // a harmless normalization that does not change which file is read. function resolveLocalPath(p) { if (typeof p !== 'string' || p.indexOf(String.fromCharCode(0)) !== -1) { throw new Error('Invalid local path: null bytes are not allowed.'); } return path.resolve(p); }