mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
fix(packaging): harden Flatpak launcher validation
This commit is contained in:
1 parent
1a5ab50872
commit
7df496505d
5 files changed
+245
-71
No files matched your search
@@ -312,6 +312,10 @@ test('package layout verifier uses canonical helpers and direct dependencies', (
|
||||
packageLayoutVerifier,
|
||||
/const\s*{\s*resolveLinuxLauncherLayout\s*}\s*=\s*require\(['"]\.\/linux-launcher-layout\.cjs['"]\)/
|
||||
);
|
||||
assert.match(
|
||||
packageLayoutVerifier,
|
||||
/const\s*{\s*validateFlatpakLauncher\s*,?\s*}\s*=\s*require\(['"]\.\/flatpak-launcher-validation\.cjs['"]\)/
|
||||
);
|
||||
assert.match(
|
||||
packageLayoutVerifier,
|
||||
/function verifyLinuxLauncher\(\s*resourceDir,\s*targetNames,\s*errors\s*\)/
|
||||
@@ -324,16 +328,6 @@ test('package layout verifier uses canonical helpers and direct dependencies', (
|
||||
packageLayoutVerifier,
|
||||
/verifyLinuxLauncher\(\s*resourceDir,\s*linuxTargetNames,\s*errors\s*\)/
|
||||
);
|
||||
assert.match(packageLayoutVerifier, /const elfMagic = Buffer\.alloc\(4\)/);
|
||||
assert.match(
|
||||
packageLayoutVerifier,
|
||||
/fs\.openSync\(launcherPath,\s*['"]r['"]\)/
|
||||
);
|
||||
assert.match(
|
||||
packageLayoutVerifier,
|
||||
/fs\.readSync\(\s*descriptor,\s*elfMagic,\s*0,\s*elfMagic\.length,\s*0\s*\)/
|
||||
);
|
||||
assert.match(packageLayoutVerifier, /fs\.closeSync\(descriptor\)/);
|
||||
|
||||
const launcherVerifier = packageLayoutVerifier.match(
|
||||
/function verifyLinuxLauncher\([\s\S]*?\n}\n\nfunction verifyFlatpakPermissions/
|
||||
@@ -345,11 +339,7 @@ test('package layout verifier uses canonical helpers and direct dependencies', (
|
||||
);
|
||||
assert.match(
|
||||
launcherVerifier,
|
||||
/if \(!launcherLayout\.wrapperRequired\) \{[\s\S]*?return;[\s\S]*?\}\s*const launcherBinaryPath[\s\S]*?fs\.readFileSync\(launcherPath,\s*['"]utf8['"]\)/
|
||||
);
|
||||
assert.match(
|
||||
launcherVerifier,
|
||||
/fs\.existsSync\(flatpakBinarySiblingPath\)/
|
||||
/if \(!launcherLayout\.wrapperRequired\) \{\s*errors\.push\(\s*\.\.\.validateFlatpakLauncher\(\s*appDir,\s*linuxExecutableName\s*\)\s*\);\s*return;\s*\}\s*const launcherBinaryPath[\s\S]*?fs\.readFileSync\(launcherPath,\s*['"]utf8['"]\)/
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const expectedElfMagic = Buffer.from([0x7f, 0x45, 0x4c, 0x46]);
|
||||
const executableModeBits = 0o111;
|
||||
|
||||
function fsErrorCode(error) {
|
||||
return error && typeof error === 'object' && typeof error.code === 'string'
|
||||
? error.code
|
||||
: 'UNKNOWN';
|
||||
}
|
||||
|
||||
function validateBinarySibling(siblingPath, errors) {
|
||||
try {
|
||||
fs.lstatSync(siblingPath);
|
||||
} catch (error) {
|
||||
if (fsErrorCode(error) === 'ENOENT') {
|
||||
return;
|
||||
}
|
||||
errors.push(
|
||||
`Unable to inspect Flatpak Electron launcher binary sibling at ${siblingPath} (${fsErrorCode(error)}).`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
errors.push(
|
||||
`Flatpak Electron layout must not include a launcher binary sibling: ${siblingPath}`
|
||||
);
|
||||
}
|
||||
|
||||
function validateFlatpakLauncher(appDir, executableName) {
|
||||
const errors = [];
|
||||
const launcherPath = path.join(appDir, executableName);
|
||||
const flatpakBinarySiblingPath = `${launcherPath}.bin`;
|
||||
validateBinarySibling(flatpakBinarySiblingPath, errors);
|
||||
|
||||
let descriptor;
|
||||
try {
|
||||
descriptor = fs.openSync(
|
||||
launcherPath,
|
||||
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW
|
||||
);
|
||||
} catch (error) {
|
||||
const errorCode = fsErrorCode(error);
|
||||
if (errorCode === 'ENOENT') {
|
||||
errors.push(
|
||||
`Missing Flatpak Electron ELF in ${appDir}: ${path.basename(launcherPath)}`
|
||||
);
|
||||
} else if (errorCode === 'ELOOP') {
|
||||
errors.push(
|
||||
`Flatpak Electron launcher must be a regular file: ${launcherPath}`
|
||||
);
|
||||
} else {
|
||||
errors.push(
|
||||
`Unable to open Flatpak Electron launcher at ${launcherPath} (${errorCode}).`
|
||||
);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
try {
|
||||
let launcherStat;
|
||||
try {
|
||||
launcherStat = fs.fstatSync(descriptor);
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`Unable to stat Flatpak Electron launcher at ${launcherPath} (${fsErrorCode(error)}).`
|
||||
);
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (!launcherStat.isFile()) {
|
||||
errors.push(
|
||||
`Flatpak Electron launcher must be a regular file: ${launcherPath}`
|
||||
);
|
||||
return errors;
|
||||
}
|
||||
if ((launcherStat.mode & executableModeBits) === 0) {
|
||||
errors.push(
|
||||
`Flatpak Electron launcher must be executable: ${launcherPath}`
|
||||
);
|
||||
}
|
||||
|
||||
const elfMagic = Buffer.alloc(expectedElfMagic.length);
|
||||
let bytesRead;
|
||||
try {
|
||||
bytesRead = fs.readSync(
|
||||
descriptor,
|
||||
elfMagic,
|
||||
0,
|
||||
elfMagic.length,
|
||||
0
|
||||
);
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`Unable to read Flatpak Electron launcher at ${launcherPath} (${fsErrorCode(error)}).`
|
||||
);
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (
|
||||
bytesRead !== expectedElfMagic.length ||
|
||||
!elfMagic.equals(expectedElfMagic)
|
||||
) {
|
||||
errors.push(
|
||||
`Flatpak Electron launcher must be an ELF binary: ${launcherPath}`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
fs.closeSync(descriptor);
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`Unable to close Flatpak Electron launcher at ${launcherPath} (${fsErrorCode(error)}).`
|
||||
);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateFlatpakLauncher,
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const {
|
||||
validateFlatpakLauncher,
|
||||
} = require('./flatpak-launcher-validation.cjs');
|
||||
|
||||
const executableName = 'iptvnator';
|
||||
const electronElf = Buffer.from([
|
||||
0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00,
|
||||
]);
|
||||
|
||||
async function createFixture(t) {
|
||||
const appDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), 'iptvnator-flatpak-launcher-')
|
||||
);
|
||||
t.after(() => fs.rm(appDir, { recursive: true, force: true }));
|
||||
|
||||
return {
|
||||
appDir,
|
||||
launcherPath: path.join(appDir, executableName),
|
||||
siblingPath: path.join(appDir, `${executableName}.bin`),
|
||||
};
|
||||
}
|
||||
|
||||
async function writeLauncher(launcherPath, contents, mode) {
|
||||
await fs.writeFile(launcherPath, contents);
|
||||
await fs.chmod(launcherPath, mode);
|
||||
}
|
||||
|
||||
test('accepts a regular executable Flatpak Electron ELF', async (t) => {
|
||||
const fixture = await createFixture(t);
|
||||
await writeLauncher(fixture.launcherPath, electronElf, 0o755);
|
||||
|
||||
assert.deepEqual(
|
||||
validateFlatpakLauncher(fixture.appDir, executableName),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects a symlinked Flatpak Electron launcher', async (t) => {
|
||||
const fixture = await createFixture(t);
|
||||
const electronTargetPath = path.join(fixture.appDir, 'electron-target');
|
||||
await writeLauncher(electronTargetPath, electronElf, 0o755);
|
||||
await fs.symlink(electronTargetPath, fixture.launcherPath);
|
||||
|
||||
assert.deepEqual(validateFlatpakLauncher(fixture.appDir, executableName), [
|
||||
`Flatpak Electron launcher must be a regular file: ${fixture.launcherPath}`,
|
||||
]);
|
||||
});
|
||||
|
||||
test('rejects a dangling Flatpak launcher binary sibling', async (t) => {
|
||||
const fixture = await createFixture(t);
|
||||
await writeLauncher(fixture.launcherPath, electronElf, 0o755);
|
||||
await fs.symlink('missing-electron', fixture.siblingPath);
|
||||
|
||||
assert.deepEqual(validateFlatpakLauncher(fixture.appDir, executableName), [
|
||||
`Flatpak Electron layout must not include a launcher binary sibling: ${fixture.siblingPath}`,
|
||||
]);
|
||||
});
|
||||
|
||||
test('rejects a non-executable regular Flatpak Electron ELF', async (t) => {
|
||||
const fixture = await createFixture(t);
|
||||
await writeLauncher(fixture.launcherPath, electronElf, 0o644);
|
||||
|
||||
assert.deepEqual(validateFlatpakLauncher(fixture.appDir, executableName), [
|
||||
`Flatpak Electron launcher must be executable: ${fixture.launcherPath}`,
|
||||
]);
|
||||
});
|
||||
|
||||
for (const [description, contents] of [
|
||||
['short ELF magic', Buffer.from([0x7f, 0x45, 0x4c])],
|
||||
['incorrect ELF magic', Buffer.from([0x00, 0x45, 0x4c, 0x46])],
|
||||
]) {
|
||||
test(`rejects ${description} in the Flatpak Electron launcher`, async (t) => {
|
||||
const fixture = await createFixture(t);
|
||||
await writeLauncher(fixture.launcherPath, contents, 0o755);
|
||||
|
||||
assert.deepEqual(
|
||||
validateFlatpakLauncher(fixture.appDir, executableName),
|
||||
[
|
||||
`Flatpak Electron launcher must be an ELF binary: ${fixture.launcherPath}`,
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test('packaging Nx tests register the Flatpak launcher validation suite', async () => {
|
||||
const project = JSON.parse(
|
||||
await fs.readFile(new URL('./project.json', import.meta.url), 'utf8')
|
||||
);
|
||||
const moduleFile =
|
||||
'{workspaceRoot}/tools/packaging/flatpak-launcher-validation.cjs';
|
||||
const testFile =
|
||||
'{workspaceRoot}/tools/packaging/flatpak-launcher-validation.test.mjs';
|
||||
|
||||
assert.ok(project.targets.test.inputs.includes(moduleFile));
|
||||
assert.ok(project.targets.test.inputs.includes(testFile));
|
||||
assert.match(
|
||||
project.targets.test.options.command,
|
||||
/node --test .*tools\/packaging\/flatpak-launcher-validation\.test\.mjs/
|
||||
);
|
||||
});
|
||||
@@ -22,6 +22,8 @@
|
||||
"{workspaceRoot}/tools/packaging/asar-dependency-closure.test.mjs",
|
||||
"{workspaceRoot}/tools/packaging/embedded-mpv-packaging.cjs",
|
||||
"{workspaceRoot}/tools/packaging/embedded-mpv-arch.test.mjs",
|
||||
"{workspaceRoot}/tools/packaging/flatpak-launcher-validation.cjs",
|
||||
"{workspaceRoot}/tools/packaging/flatpak-launcher-validation.test.mjs",
|
||||
"{workspaceRoot}/tools/packaging/configure-linux-frame-copy-build.mjs",
|
||||
"{workspaceRoot}/tools/packaging/configure-linux-frame-copy-build.test.mjs",
|
||||
"{workspaceRoot}/tools/packaging/linux-after-pack.test.mjs",
|
||||
@@ -50,7 +52,7 @@
|
||||
"{workspaceRoot}/tools/embedded-mpv/stage-windows-runtime-archive.mjs"
|
||||
],
|
||||
"options": {
|
||||
"command": "node --test tools/packaging/electron-package-identity.test.mjs tools/packaging/asar-dependency-closure.test.mjs tools/packaging/embedded-mpv-arch.test.mjs tools/packaging/configure-linux-frame-copy-build.test.mjs tools/packaging/linux-after-pack.test.mjs tools/packaging/linux-frame-copy-profile.test.mjs tools/packaging/prepare-linux-runtime-source-snapshot.test.mjs tools/packaging/publish-snap-workflow.test.mjs tools/packaging/release-snap-assets.test.mjs tools/packaging/verify-linux-frame-copy-runtime.test.mjs tools/embedded-mpv/build-linux-runtime.test.mjs tools/embedded-mpv/generate-linux-runtime-notices.test.mjs tools/embedded-mpv/linux-runtime-manifest.test.mjs",
|
||||
"command": "node --test tools/packaging/electron-package-identity.test.mjs tools/packaging/asar-dependency-closure.test.mjs tools/packaging/embedded-mpv-arch.test.mjs tools/packaging/flatpak-launcher-validation.test.mjs tools/packaging/configure-linux-frame-copy-build.test.mjs tools/packaging/linux-after-pack.test.mjs tools/packaging/linux-frame-copy-profile.test.mjs tools/packaging/prepare-linux-runtime-source-snapshot.test.mjs tools/packaging/publish-snap-workflow.test.mjs tools/packaging/release-snap-assets.test.mjs tools/packaging/verify-linux-frame-copy-runtime.test.mjs tools/embedded-mpv/build-linux-runtime.test.mjs tools/embedded-mpv/generate-linux-runtime-notices.test.mjs tools/embedded-mpv/linux-runtime-manifest.test.mjs",
|
||||
"cwd": "{workspaceRoot}"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -19,6 +19,9 @@ const {
|
||||
validateLinuxProfileTargets,
|
||||
} = require('./linux-frame-copy-profile.cjs');
|
||||
const { resolveLinuxLauncherLayout } = require('./linux-launcher-layout.cjs');
|
||||
const {
|
||||
validateFlatpakLauncher,
|
||||
} = require('./flatpak-launcher-validation.cjs');
|
||||
const args = process.argv.slice(2);
|
||||
const normalizedArgs = args[0] === '--' ? args.slice(1) : args;
|
||||
const [platform, arch = ''] = normalizedArgs;
|
||||
@@ -489,61 +492,7 @@ function verifyLinuxLauncher(resourceDir, targetNames, errors) {
|
||||
const launcherPath = path.join(appDir, linuxExecutableName);
|
||||
|
||||
if (!launcherLayout.wrapperRequired) {
|
||||
if (!fileExists(launcherPath)) {
|
||||
errors.push(
|
||||
`Missing Flatpak Electron ELF in ${appDir}: ${path.basename(launcherPath)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const flatpakBinarySiblingPath = `${launcherPath}.bin`;
|
||||
if (fs.existsSync(flatpakBinarySiblingPath)) {
|
||||
errors.push(
|
||||
`Flatpak Electron layout must not include a launcher binary sibling: ${flatpakBinarySiblingPath}`
|
||||
);
|
||||
}
|
||||
|
||||
const expectedElfMagic = Buffer.from([0x7f, 0x45, 0x4c, 0x46]);
|
||||
const elfMagic = Buffer.alloc(4);
|
||||
let descriptor;
|
||||
try {
|
||||
descriptor = fs.openSync(launcherPath, 'r');
|
||||
const bytesRead = fs.readSync(
|
||||
descriptor,
|
||||
elfMagic,
|
||||
0,
|
||||
elfMagic.length,
|
||||
0
|
||||
);
|
||||
if (
|
||||
bytesRead !== expectedElfMagic.length ||
|
||||
!elfMagic.equals(expectedElfMagic)
|
||||
) {
|
||||
errors.push(
|
||||
`Flatpak Electron launcher must be an ELF binary: ${launcherPath}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`Unable to inspect Flatpak Electron ELF at ${launcherPath}: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
} finally {
|
||||
if (descriptor !== undefined) {
|
||||
try {
|
||||
fs.closeSync(descriptor);
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`Unable to close Flatpak Electron ELF at ${launcherPath}: ${
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: String(error)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
errors.push(...validateFlatpakLauncher(appDir, linuxExecutableName));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user