mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
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
This commit is contained in:
@@ -172,10 +172,15 @@ async function downloadPandoc() {
|
|||||||
async function downloadFiraCode() {
|
async function downloadFiraCode() {
|
||||||
const targetDir = path.resolve(__dirname, '..', 'assets', 'fonts');
|
const targetDir = path.resolve(__dirname, '..', 'assets', 'fonts');
|
||||||
fs.mkdirSync(targetDir, { recursive: true });
|
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 = [
|
const files = [
|
||||||
{ url: 'https://github.com/tonsky/FiraCode/raw/master/distr/ttf/FiraCode-Regular.ttf', out: 'FiraCode-Regular.ttf' },
|
{ url: `${baseUrl}/FiraCode-Regular.ttf`, out: 'FiraCode-Regular.ttf', sha256: '3c79d234a9161c790410ebb2a80de7efb7c15f581062c130e0fa78503ccdd0da' },
|
||||||
{ url: 'https://github.com/tonsky/FiraCode/raw/master/distr/ttf/FiraCode-Bold.ttf', out: 'FiraCode-Bold.ttf' },
|
{ url: `${baseUrl}/FiraCode-Bold.ttf`, out: 'FiraCode-Bold.ttf', sha256: '975f26779fac1029c2cbdac1e9fac7e9ddeec05e064675e4aac63bffa121742f' },
|
||||||
{ url: 'https://raw.githubusercontent.com/tonsky/FiraCode/master/LICENSE', out: 'FiraCode-LICENSE.txt' },
|
{ url: `${baseUrl}/FiraCode-LICENSE.txt`, out: 'FiraCode-LICENSE.txt', sha256: null },
|
||||||
];
|
];
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
const dest = path.join(targetDir, f.out);
|
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.`);
|
console.log(`[download-tools] Fira Code asset already present at ${dest} — skipping.`);
|
||||||
continue;
|
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}...`);
|
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');
|
console.log('[download-tools] Fira Code ready');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ const fs = require('fs');
|
|||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
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) {
|
function buildPdfFontHeader(settings, ttfPath, fontFamily) {
|
||||||
if (!ttfPath || !fs.existsSync(ttfPath)) {
|
if (!ttfPath || !fs.existsSync(ttfPath)) {
|
||||||
throw new Error(`PdfFontHeader: TTF not found at ${ttfPath}`);
|
throw new Error(`PdfFontHeader: TTF not found at ${ttfPath}`);
|
||||||
@@ -21,9 +29,10 @@ function buildPdfFontHeader(settings, ttfPath, fontFamily) {
|
|||||||
` Scale = 0.9]`,
|
` Scale = 0.9]`,
|
||||||
`{${fontFamily}}`,
|
`{${fontFamily}}`,
|
||||||
].filter(Boolean);
|
].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');
|
fs.writeFileSync(headerPath, lines.join('\n'), 'utf-8');
|
||||||
return { headerPath, familyName: fontFamily };
|
return { headerPath, dir, familyName: fontFamily };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { buildPdfFontHeader };
|
module.exports = { buildPdfFontHeader };
|
||||||
|
|||||||
+20
-2
@@ -1600,14 +1600,16 @@ function performExportWithOptions(format, options) {
|
|||||||
// active monospace font (xelatex fontspec) so ASCII art renders
|
// active monospace font (xelatex fontspec) so ASCII art renders
|
||||||
// identically across machines.
|
// identically across machines.
|
||||||
const monoCtx = getActiveMonospaceContext();
|
const monoCtx = getActiveMonospaceContext();
|
||||||
|
let monoPdfHeaderDir = null;
|
||||||
if (monoCtx.ttf) {
|
if (monoCtx.ttf) {
|
||||||
try {
|
try {
|
||||||
const { headerPath } = buildPdfFontHeader(
|
const built = buildPdfFontHeader(
|
||||||
monoCtx.settings,
|
monoCtx.settings,
|
||||||
monoCtx.ttf,
|
monoCtx.ttf,
|
||||||
MonospaceFontConfig.getActiveFamily(monoCtx.settings)
|
MonospaceFontConfig.getActiveFamily(monoCtx.settings)
|
||||||
);
|
);
|
||||||
pandocCmd += ` --include-in-header="${headerPath}"`;
|
pandocCmd += ` --include-in-header="${built.headerPath}"`;
|
||||||
|
monoPdfHeaderDir = built.dir;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[monospace] PDF header build failed (non-fatal):', e.message);
|
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
|
// Helper function to export with pandoc (general) - uses runPandocCmd for safety
|
||||||
function exportWithPandoc(pandocCmd, outputFile, format) {
|
function exportWithPandoc(pandocCmd, outputFile, format) {
|
||||||
console.log(`Executing Pandoc command: ${pandocCmd}`);
|
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) => {
|
runPandocCmd(pandocCmd, async (error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -1971,6 +1986,9 @@ function exportWithPandoc(pandocCmd, outputFile, format) {
|
|||||||
// Headers/footers are applied only for DOCX format
|
// Headers/footers are applied only for DOCX format
|
||||||
|
|
||||||
showExportSuccess(outputFile);
|
showExportSuccess(outputFile);
|
||||||
|
cleanupMonoHeader();
|
||||||
|
} else {
|
||||||
|
cleanupMonoHeader();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ describe('PdfFontHeader', () => {
|
|||||||
expect(content).toContain('\\setmonofont');
|
expect(content).toContain('\\setmonofont');
|
||||||
expect(content).toContain('JetBrainsMono-Regular.ttf');
|
expect(content).toContain('JetBrainsMono-Regular.ttf');
|
||||||
expect(out.familyName).toBe('JetBrains Mono');
|
expect(out.familyName).toBe('JetBrains Mono');
|
||||||
|
fs.rmSync(out.dir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('writes header with ligatures when enabled', () => {
|
test('writes header with ligatures when enabled', () => {
|
||||||
@@ -39,6 +40,7 @@ describe('PdfFontHeader', () => {
|
|||||||
);
|
);
|
||||||
const content = fs.readFileSync(out.headerPath, 'utf-8');
|
const content = fs.readFileSync(out.headerPath, 'utf-8');
|
||||||
expect(content).toContain('Ligatures=TeX');
|
expect(content).toContain('Ligatures=TeX');
|
||||||
|
fs.rmSync(out.dir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('header is written under os.tmpdir', () => {
|
test('header is written under os.tmpdir', () => {
|
||||||
@@ -50,6 +52,7 @@ describe('PdfFontHeader', () => {
|
|||||||
'JetBrains Mono'
|
'JetBrains Mono'
|
||||||
);
|
);
|
||||||
expect(out.headerPath.startsWith(os.tmpdir())).toBe(true);
|
expect(out.headerPath.startsWith(os.tmpdir())).toBe(true);
|
||||||
|
fs.rmSync(out.dir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('throws if ttfPath does not exist', () => {
|
test('throws if ttfPath does not exist', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user