fix: complete marketplace client remediation

This commit is contained in:
2026-08-28 23:14:41 +08:00
parent 2c3baf6dff
commit 11d0af0166
18 changed files with 1007 additions and 90 deletions

View File

@@ -322,16 +322,67 @@ export function verifyMarketplaceClientArtifact(appAsarContents, productionTrust
* Checkout sources are not evidence of what an installed app will trust.
*/
export async function readPackagedMarketplaceTrustSource(appAsar) {
const candidates = listPackage(appAsar, { isPack: false })
.filter((entry) => /\.(?:js|mjs|cjs)$/u.test(entry));
for (const entry of candidates) {
const filename = entry.replace(/^[/\\]+/u, '');
const entries = new Set(listPackage(appAsar, { isPack: false })
.map((entry) => entry.replace(/^[/\\]+/u, '').replaceAll('\\', '/')));
const readArchiveText = (entry) => Buffer.from(
extractFile(appAsar, entry.replaceAll('/', sep)),
).toString('utf8');
let packageJson;
try {
packageJson = JSON.parse(readArchiveText('package.json'));
} catch {
throw new Error('Packaged app.asar package.json is unreadable');
}
if (typeof packageJson.main !== 'string' || packageJson.main.trim().length === 0) {
throw new Error('Packaged app.asar package.json.main is missing');
}
const normalizeAsarPath = (value) => {
const result = [];
for (const segment of value.replaceAll('\\', '/').split('/')) {
if (!segment || segment === '.') continue;
if (segment === '..') {
if (result.length === 0) return null;
result.pop();
} else result.push(segment);
}
return result.join('/');
};
const resolveModule = (from, specifier) => {
if (!specifier.startsWith('.')) return null;
const base = normalizeAsarPath(`${dirname(from).replaceAll('\\', '/')}/${specifier}`);
if (!base) return null;
const candidates = [base, `${base}.js`, `${base}.mjs`, `${base}.cjs`, `${base}/index.js`];
return candidates.find((candidate) => entries.has(candidate)) ?? null;
};
const main = normalizeAsarPath(packageJson.main);
if (!main || !entries.has(main)) throw new Error('Packaged app.asar package.json.main is not present');
const pending = [main];
const reachable = [];
const seen = new Set();
while (pending.length > 0) {
const entry = pending.shift();
if (!entry || seen.has(entry)) continue;
seen.add(entry);
let source;
try {
source = extractFile(appAsar, filename).toString('utf8');
source = readArchiveText(entry);
} catch {
continue;
}
reachable.push({ entry, source });
const specifiers = [];
const importPattern = /(?:import|export)\s+(?:[\s\S]*?\sfrom\s*)?['"]([^'"]+)['"]/gu;
const requirePattern = /\brequire\(\s*['"]([^'"]+)['"]\s*\)/gu;
for (const match of source.matchAll(importPattern)) specifiers.push(match[1]);
for (const match of source.matchAll(requirePattern)) specifiers.push(match[1]);
for (const specifier of specifiers) {
const resolved = resolveModule(entry, specifier);
if (resolved) pending.push(resolved);
}
}
for (const { source } of reachable) {
const trustAssignment = source.match(
/CODE_OWNED_PLUGIN_SIGNING_KEYS\s*=\s*Object\.freeze\(\s*\{\}\s*(?:as\s+[^)]*)?\)/u,
);