UNPKG

pdf-to-png-converter

Version:

Node.js utility to convert PDF file/buffer pages to PNG files/buffers. No build-time compilation required — pre-built native binaries included for all major platforms.

89 lines (88 loc) 4.98 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.resolveOutputFolder = resolveOutputFolder; exports.prepareOutputFolder = prepareOutputFolder; exports.savePNGfile = savePNGfile; const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); const flatFilename_js_1 = require("./flatFilename.js"); function isEscapingRelativePath(rel) { return rel === '..' || rel.startsWith('..' + node_path_1.sep) || (0, node_path_1.isAbsolute)(rel); } /** * Resolves `outputFolder` against the process CWD. Kept separate from `prepareOutputFolder` so the * CWD is captured at conversion start, before any user-supplied `outputFileMaskFunc` runs: a mask * callback that calls `process.chdir()` must not be able to redirect where a relative * `outputFolder` lands. */ function resolveOutputFolder(outputFolder) { return (0, node_path_1.resolve)(outputFolder); } /** * Creates the already-resolved output folder and captures its `realpath` as the baseline every * subsequent write is checked against. Colocated with `savePNGfile` so the whole SEC-001/002/003 * threat model — folder creation, the realpath baseline, and the per-write re-check that consumes * it — lives in this one module. * * Callers must reject duplicate output filenames BEFORE calling this: it is the first output I/O * of a conversion, and running it earlier would leave a created directory behind on a conversion * that then fails validation. */ async function prepareOutputFolder(resolvedOutputFolder) { await node_fs_1.promises.mkdir(resolvedOutputFolder, { recursive: true }); const realOutputFolder = await node_fs_1.promises.realpath(resolvedOutputFolder); return { resolvedOutputFolder, realOutputFolder }; } /** * Writes a rendered PNG page to disk using an exclusive-create open (`'wx'`) and returns the final path. * * The `name` argument must be a flat filename containing no host path separators — `/` on * POSIX, and both `/` and `\` on Windows. On POSIX, `\` is a valid filename character and is * intentionally allowed (e.g. PDFs named `foo\bar.pdf` produce a default mask of `foo\bar`). * Rejecting separators here closes the TOCTOU window on intermediate directory components (an * attacker with write access to the output folder could otherwise swap a sub-directory for a * symlink between the realpath check and the `open()` call). The `'wx'` flag additionally * prevents overwriting an existing target and blocks following a pre-existing symlink at the * target filename on POSIX systems. Because the filename is flat, the file's directory IS the * output folder itself, so a single fresh `realpath` of the output folder immediately before * `open()` — compared for exact equality with the value captured at conversion start — detects * any symlink swap or rename of the folder or its ancestors in one syscall (equality is strictly * stronger than a containment check). Callers should clear the output folder before re-running * the same conversion if they expect to reuse the same output names. The input object is not * mutated; callers receive the resolved path from the return value. */ async function savePNGfile(name, content, folder) { const { resolvedOutputFolder, realOutputFolder } = folder; if ((0, flatFilename_js_1.containsPathSeparator)(name)) { throw new Error(`Output file name must be a flat filename without ${flatFilename_js_1.SEPARATOR_DESCRIPTION} path separators: ${name}`); } if ((0, node_path_1.isAbsolute)(name)) { throw new Error(`Output file name escapes the output folder: ${name}`); } // '.' collapses to the output folder itself under join() (bypassing the escape checks below, // since relative() yields ''), and would surface as a raw EEXIST/EISDIR from open() that // leaks the absolute folder path. '..' needs no twin guard: it resolves to the PARENT folder, // which the escaping-relative check below already rejects cleanly. if (name === '.') { throw new Error(`Output file name must be a plain filename, received: ${name}`); } const resolvedFilePath = (0, node_path_1.join)(resolvedOutputFolder, name); if (isEscapingRelativePath((0, node_path_1.relative)(resolvedOutputFolder, resolvedFilePath))) { throw new Error(`Output file name escapes the output folder: ${name}`); } if (!Buffer.isBuffer(content)) { throw new Error(`Cannot write PNG file "${resolvedFilePath}" because content is not a Buffer.`); } const realOutputFolderFinal = await node_fs_1.promises.realpath(resolvedOutputFolder); if (realOutputFolderFinal !== realOutputFolder) { throw new Error(`Output folder was modified during write: ${resolvedOutputFolder}`); } const fd = await node_fs_1.promises.open(resolvedFilePath, 'wx'); try { await fd.writeFile(content); } finally { await fd.close(); } return resolvedFilePath; }