can1357/oh-my-pi · critical · Error
Unsafe embedded addon filename: ${file.filename}
Error message
Unsafe embedded addon filename: ${file.filename} What it means
extractEmbeddedAddonArchive validates every filename in the embedded-addon manifest before doing any I/O. A filename is unsafe if it is empty, contains a path separator ('/' or '\\'), or differs from its own path.basename — i.e. it could escape the target directory. The library throws this to prevent path traversal when writing files extracted from the bundled tar.gz into the native version directory.
Source
Thrown at packages/natives/native/loader-state.js:501
const tempPath = `${targetPath}.tmp.${process.pid}.${Date.now()}`;
try {
fs.writeFileSync(tempPath, content, { mode: 0o755 });
fs.renameSync(tempPath, targetPath);
} catch (err) {
try {
fs.unlinkSync(tempPath);
} catch {
// Best-effort cleanup only.
}
throw err;
}
}
export function extractEmbeddedAddonArchive({ archivePath, files, targetDir }) {
const pending = new Map();
for (const file of files) {
if (!isSafeEmbeddedAddonFilename(file.filename)) {
throw new Error(`Unsafe embedded addon filename: ${file.filename}`);
}
const targetPath = path.join(targetDir, file.filename);
if (!isEmbeddedAddonFileCurrent(targetPath, file)) {
pending.set(file.filename, file);
}
}
if (pending.size === 0) return [];
const archive = zlib.gunzipSync(fs.readFileSync(archivePath));
const writtenPaths = [];
let offset = 0;
while (offset + 512 <= archive.length) {
if (isZeroTarBlock(archive, offset)) break;
const header = archive.subarray(offset, offset + 512);
const filename = getTarEntryName(header);
const size = readTarOctal(header, 124, 12);
const typeflag = header[156] === 0 ? "0" : String.fromCharCode(header[156]);View on GitHub (pinned to 9690622007)
Solutions
- Fix the manifest so each files[].filename is a bare filename with no directory components (e.g. 'pi_natives-linux-x64.node').
- Rebuild/reinstall the compiled binary so the embedded addon metadata is regenerated from the official build.
- If you construct the manifest yourself, normalize entries with path.basename(filename) and reject entries where the result differs from the input.
Example fix
// before
extractEmbeddedAddonArchive({ archivePath, files: [{ filename: "sub/pi_natives.node" }], targetDir });
// after
const files = [{ filename: "pi_natives.node" }];
if (!files.every(f => f.filename && path.basename(f.filename) === f.filename)) throw new Error("bad manifest");
extractEmbeddedAddonArchive({ archivePath, files, targetDir }); Defensive patterns
Strategy: validation
Validate before calling
import * as path from "node:path";
function assertSafeAddonFilenames(files) {
for (const file of files) {
if (!file.filename || path.basename(file.filename) !== file.filename ||
file.filename.includes("/") || file.filename.includes("\\")) {
throw new Error(`Unsafe embedded addon filename: ${file.filename}`);
}
}
}
assertSafeAddonFilenames(files); // run before extractEmbeddedAddonArchive Type guard
function isSafeAddonFilename(filename) {
return typeof filename === "string" && filename.length > 0 &&
path.basename(filename) === filename &&
!filename.includes("/") && !filename.includes("\\");
} Try / catch
try {
extractEmbeddedAddonArchive({ archivePath, files, targetDir });
} catch (err) {
if (String(err.message).startsWith("Unsafe embedded addon filename:")) {
// reject/treat manifest as untrusted; skip embedded path and fall back to disk lookup
} else throw err;
} Prevention
- Always derive manifest filenames from path.basename at packaging time.
- Validate any externally sourced file lists against a basename-only allowlist before use.
- Treat embedded archive metadata as untrusted input; never concatenate paths from it without basename checks.
- Add a packaging CI check that fails if any manifest filename contains a separator.
When it happens
Trigger: Calling extractEmbeddedAddonArchive({archivePath, files, targetDir}) where any entry of files has file.filename that is empty, absolute, or contains '/' or '\\' (e.g. '../evil.node' or 'sub/dir/x.node'). The check runs before the archive is even read, so a single bad manifest entry aborts extraction.
Common situations: A corrupted or hand-edited embedded-addon manifest; a custom build pipeline that records filenames with directory prefixes; a tampered/partially overwritten compiled binary's embedded metadata; tests feeding synthetic manifest data with paths.
Related errors
- Destination paths cannot contain parent traversal or NUL byt
- Shared-folder destination escapes its configured root
- Absolute paths are not allowed in ${scheme}:// URLs: ${rawPa
- Path traversal (..) is not allowed in ${scheme}:// URLs: ${r
- Unsafe #{scheme}:// path (absolute or traversal): #{path}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/f0dcd2e2eac5d5c5.
Report an issue: GitHub.