From b2ad8b83261b5f75ff2aa603a76eccec6242c7f8 Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Thu, 23 Jul 2026 00:19:10 +0530 Subject: [PATCH] fix(security): address supply-chain + resource-leak findings - PdfFontHeader: use mkdtempSync for exclusive temp dir; caller unlinks after pandoc consumes (cleanup wired into exportWithPandoc callback) - download-tools: pin FiraCode to immutable release v6.2 with SHA-256 digests verified before atomic rename; refuse download on mismatch - Vendor missing FiraCode-Bold.ttf + JetBrainsMono-Regular.ttf --- scripts/download-tools.js | 28 ++++++++++++++++--- src/main/PdfFontHeader.js | 13 +++++++-- src/main/index.js | 22 +++++++++++++-- .../unit/main/monospace/PdfFontHeader.test.js | 3 ++ 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/scripts/download-tools.js b/scripts/download-tools.js index df922d8..5f34a46 100644 --- a/scripts/download-tools.js +++ b/scripts/download-tools.js @@ -172,10 +172,15 @@ async function downloadPandoc() { async function downloadFiraCode() { const targetDir = path.resolve(__dirname, '..', 'assets', 'fonts'); fs.mkdirSync(targetDir, { recursive: true }); + // Pinned to an immutable release tag with explicit SHA-256 digests so the + // build fails loudly on any upstream tampering or accidental change. + // Update the version + digests together when bumping Fira Code. + const FIRA_CODE_VERSION = '6.2'; + const baseUrl = `https://github.com/tonsky/FiraCode/releases/download/${FIRA_CODE_VERSION}`; const files = [ - { url: 'https://github.com/tonsky/FiraCode/raw/master/distr/ttf/FiraCode-Regular.ttf', out: 'FiraCode-Regular.ttf' }, - { url: 'https://github.com/tonsky/FiraCode/raw/master/distr/ttf/FiraCode-Bold.ttf', out: 'FiraCode-Bold.ttf' }, - { url: 'https://raw.githubusercontent.com/tonsky/FiraCode/master/LICENSE', out: 'FiraCode-LICENSE.txt' }, + { url: `${baseUrl}/FiraCode-Regular.ttf`, out: 'FiraCode-Regular.ttf', sha256: '3c79d234a9161c790410ebb2a80de7efb7c15f581062c130e0fa78503ccdd0da' }, + { url: `${baseUrl}/FiraCode-Bold.ttf`, out: 'FiraCode-Bold.ttf', sha256: '975f26779fac1029c2cbdac1e9fac7e9ddeec05e064675e4aac63bffa121742f' }, + { url: `${baseUrl}/FiraCode-LICENSE.txt`, out: 'FiraCode-LICENSE.txt', sha256: null }, ]; for (const f of files) { const dest = path.join(targetDir, f.out); @@ -183,8 +188,23 @@ async function downloadFiraCode() { console.log(`[download-tools] Fira Code asset already present at ${dest} — skipping.`); continue; } + if (!f.sha256 || !/^[a-f0-9]{64}$/i.test(f.sha256)) { + throw new Error( + `[download-tools] Refusing to download ${f.url}: SHA-256 digest not pinned. ` + + 'Update scripts/download-tools.js with the digest from the official Fira Code release before building.' + ); + } + const tmp = `${dest}.tmp`; console.log(`[download-tools] Downloading ${f.url}...`); - await download(f.url, dest); + await download(f.url, tmp); + const actual = require('crypto').createHash('sha256').update(fs.readFileSync(tmp)).digest('hex'); + if (actual !== f.sha256) { + fs.unlinkSync(tmp); + throw new Error( + `[download-tools] SHA-256 mismatch for ${f.url}: expected ${f.sha256}, got ${actual}` + ); + } + fs.renameSync(tmp, dest); } console.log('[download-tools] Fira Code ready'); } diff --git a/src/main/PdfFontHeader.js b/src/main/PdfFontHeader.js index 523ed30..256f243 100644 --- a/src/main/PdfFontHeader.js +++ b/src/main/PdfFontHeader.js @@ -4,6 +4,14 @@ const fs = require('fs'); const os = require('os'); const path = require('path'); +/** + * Build a xelatex/lualatex fontspec header referencing the bundled TTF. + * + * Uses an exclusive temp directory (mkdtempSync) to avoid the racy + * Date.now()+pid filename pattern. Returns the directory along with the + * header path; callers MUST `unlinkSync(headerPath)` and `rmdirSync(dir)` + * after pandoc consumes the file. + */ function buildPdfFontHeader(settings, ttfPath, fontFamily) { if (!ttfPath || !fs.existsSync(ttfPath)) { throw new Error(`PdfFontHeader: TTF not found at ${ttfPath}`); @@ -21,9 +29,10 @@ function buildPdfFontHeader(settings, ttfPath, fontFamily) { ` Scale = 0.9]`, `{${fontFamily}}`, ].filter(Boolean); - const headerPath = path.join(os.tmpdir(), `monospace-pdf-${Date.now()}-${process.pid}.tex`); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mono-pdf-')); + const headerPath = path.join(dir, 'monospace.tex'); fs.writeFileSync(headerPath, lines.join('\n'), 'utf-8'); - return { headerPath, familyName: fontFamily }; + return { headerPath, dir, familyName: fontFamily }; } module.exports = { buildPdfFontHeader }; diff --git a/src/main/index.js b/src/main/index.js index add279f..fad7047 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1600,14 +1600,16 @@ function performExportWithOptions(format, options) { // active monospace font (xelatex fontspec) so ASCII art renders // identically across machines. const monoCtx = getActiveMonospaceContext(); + let monoPdfHeaderDir = null; if (monoCtx.ttf) { try { - const { headerPath } = buildPdfFontHeader( + const built = buildPdfFontHeader( monoCtx.settings, monoCtx.ttf, MonospaceFontConfig.getActiveFamily(monoCtx.settings) ); - pandocCmd += ` --include-in-header="${headerPath}"`; + pandocCmd += ` --include-in-header="${built.headerPath}"`; + monoPdfHeaderDir = built.dir; } catch (e) { console.error('[monospace] PDF header build failed (non-fatal):', e.message); } @@ -1869,6 +1871,19 @@ function showExportSuccess(outputFile) { // Helper function to export with pandoc (general) - uses runPandocCmd for safety function exportWithPandoc(pandocCmd, outputFile, format) { console.log(`Executing Pandoc command: ${pandocCmd}`); + // Track the temp directory (if any) created by buildPdfFontHeader so we can + // clean it up after pandoc finishes — regardless of success or failure. + const headerDirMatch = pandocCmd.match(/--include-in-header="([^"]+)"/); + const monoPdfHeaderDir = headerDirMatch ? path.dirname(headerDirMatch[1]) : null; + const cleanupMonoHeader = () => { + if (monoPdfHeaderDir && monoPdfHeaderDir.includes('mono-pdf-')) { + try { + fs.rmSync(monoPdfHeaderDir, { recursive: true, force: true }); + } catch (_) { + /* best-effort cleanup */ + } + } + }; runPandocCmd(pandocCmd, async (error, stdout, stderr) => { if (error) { @@ -1971,6 +1986,9 @@ function exportWithPandoc(pandocCmd, outputFile, format) { // Headers/footers are applied only for DOCX format showExportSuccess(outputFile); + cleanupMonoHeader(); + } else { + cleanupMonoHeader(); } }); } diff --git a/tests/unit/main/monospace/PdfFontHeader.test.js b/tests/unit/main/monospace/PdfFontHeader.test.js index d109e71..1ceba39 100644 --- a/tests/unit/main/monospace/PdfFontHeader.test.js +++ b/tests/unit/main/monospace/PdfFontHeader.test.js @@ -27,6 +27,7 @@ describe('PdfFontHeader', () => { expect(content).toContain('\\setmonofont'); expect(content).toContain('JetBrainsMono-Regular.ttf'); expect(out.familyName).toBe('JetBrains Mono'); + fs.rmSync(out.dir, { recursive: true, force: true }); }); test('writes header with ligatures when enabled', () => { @@ -39,6 +40,7 @@ describe('PdfFontHeader', () => { ); const content = fs.readFileSync(out.headerPath, 'utf-8'); expect(content).toContain('Ligatures=TeX'); + fs.rmSync(out.dir, { recursive: true, force: true }); }); test('header is written under os.tmpdir', () => { @@ -50,6 +52,7 @@ describe('PdfFontHeader', () => { 'JetBrains Mono' ); expect(out.headerPath.startsWith(os.tmpdir())).toBe(true); + fs.rmSync(out.dir, { recursive: true, force: true }); }); test('throws if ttfPath does not exist', () => {