Skip to content

Commit bf5d116

Browse files
authored
refactor: font generation to use execFile for security (#4616)
Replaced usage of exec with execFile to prevent shell injection risks when running pyftsubset.
1 parent 2117383 commit bf5d116

File tree

1 file changed

+23
-10
lines changed

1 file changed

+23
-10
lines changed

packages/foundations/assets/fonts/generate-eu-fonts.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { glob } from 'glob';
2-
import { exec } from 'node:child_process';
2+
import { execFile } from 'node:child_process';
33
import { promisify } from 'node:util';
44

55
import { dirname } from 'path';
@@ -8,12 +8,16 @@ import { fileURLToPath } from 'url';
88
const __filename = fileURLToPath(import.meta.url);
99
const __dirname = dirname(__filename).replaceAll('\\', '/');
1010

11-
const execAsync = promisify(exec);
11+
// Security: Using execFile instead of exec to eliminate shell injection risks
12+
// execFile directly executes the binary without involving a shell
13+
const execFileAsync = promisify(execFile);
1214

1315
const generateFonts = async () => {
1416
console.log('Generating EU fonts...');
1517
try {
16-
await execAsync('pyftsubset --help');
18+
// Security: Using array arguments instead of concatenated string
19+
// This prevents shell interpretation of special characters
20+
await execFileAsync('pyftsubset', ['--help']);
1721
} catch (e) {
1822
console.warn(
1923
'You need to install pyftsubset. Check packages/foundations/assets/fonts/README.md for more information.'
@@ -22,19 +26,28 @@ const generateFonts = async () => {
2226

2327
try {
2428
const files = await glob(`${__dirname}/*.ttf`);
25-
const commands = files.map((file) =>
26-
[
27-
'pyftsubset',
29+
30+
for (const file of files) {
31+
// Security: Validate that the file is within the expected directory
32+
// and has the expected extension to prevent path traversal attacks
33+
if (!file.startsWith(__dirname) || !file.endsWith('.ttf')) {
34+
console.warn(`Skipping potentially unsafe file path: ${file}`);
35+
continue;
36+
}
37+
38+
// Security: Arguments are passed as separate array elements
39+
// No shell concatenation means no risk of command injection
40+
const args = [
2841
file,
2942
'--layout-features=*',
3043
'--flavor=woff2',
3144
`--unicodes-file=${__dirname}/unicode-eu.txt`,
3245
`--output-file=${file.replace('.ttf', '-EU.woff2')}`
33-
].join(' ')
34-
);
46+
];
3547

36-
for (const command of commands) {
37-
const { stdout, stderr } = await execAsync(command);
48+
// Security: execFile provides better performance and type safety
49+
// as it doesn't spawn a shell process
50+
const { stdout, stderr } = await execFileAsync('pyftsubset', args);
3851
if (stdout) console.log(`stdout: ${stdout}`);
3952
if (stderr) console.error(`stderr: ${stderr}`);
4053
}

0 commit comments

Comments
 (0)