fix(security): harden npm plugin and hook install integrity flow

This commit is contained in:
Peter Steinberger
2026-02-19 15:10:57 +01:00
parent 2777d8ad93
commit 5dc50b8a3f
23 changed files with 1047 additions and 183 deletions

View File

@@ -29,6 +29,16 @@ export type PluginUpdateSummary = {
outcomes: PluginUpdateOutcome[];
};
export type PluginUpdateIntegrityDriftParams = {
pluginId: string;
spec: string;
expectedIntegrity: string;
actualIntegrity: string;
resolvedSpec?: string;
resolvedVersion?: string;
dryRun: boolean;
};
export type PluginChannelSyncSummary = {
switchedToBundled: string[];
switchedToNpm: string[];
@@ -143,6 +153,7 @@ export async function updateNpmInstalledPlugins(params: {
pluginIds?: string[];
skipIds?: Set<string>;
dryRun?: boolean;
onIntegrityDrift?: (params: PluginUpdateIntegrityDriftParams) => boolean | Promise<boolean>;
}): Promise<PluginUpdateSummary> {
const logger = params.logger ?? {};
const installs = params.config.plugins?.installs ?? {};
@@ -210,6 +221,25 @@ export async function updateNpmInstalledPlugins(params: {
mode: "update",
dryRun: true,
expectedPluginId: pluginId,
expectedIntegrity: record.integrity,
onIntegrityDrift: async (drift) => {
const payload: PluginUpdateIntegrityDriftParams = {
pluginId,
spec: drift.spec,
expectedIntegrity: drift.expectedIntegrity,
actualIntegrity: drift.actualIntegrity,
resolvedSpec: drift.resolution.resolvedSpec,
resolvedVersion: drift.resolution.version,
dryRun: true,
};
if (params.onIntegrityDrift) {
return await params.onIntegrityDrift(payload);
}
logger.warn?.(
`Integrity drift for "${pluginId}" (${payload.resolvedSpec ?? payload.spec}): expected ${payload.expectedIntegrity}, got ${payload.actualIntegrity}`,
);
return true;
},
logger,
});
} catch (err) {
@@ -257,6 +287,25 @@ export async function updateNpmInstalledPlugins(params: {
spec: record.spec,
mode: "update",
expectedPluginId: pluginId,
expectedIntegrity: record.integrity,
onIntegrityDrift: async (drift) => {
const payload: PluginUpdateIntegrityDriftParams = {
pluginId,
spec: drift.spec,
expectedIntegrity: drift.expectedIntegrity,
actualIntegrity: drift.actualIntegrity,
resolvedSpec: drift.resolution.resolvedSpec,
resolvedVersion: drift.resolution.version,
dryRun: false,
};
if (params.onIntegrityDrift) {
return await params.onIntegrityDrift(payload);
}
logger.warn?.(
`Integrity drift for "${pluginId}" (${payload.resolvedSpec ?? payload.spec}): expected ${payload.expectedIntegrity}, got ${payload.actualIntegrity}`,
);
return true;
},
logger,
});
} catch (err) {
@@ -283,6 +332,12 @@ export async function updateNpmInstalledPlugins(params: {
spec: record.spec,
installPath: result.targetDir,
version: nextVersion,
resolvedName: result.npmResolution?.name,
resolvedVersion: result.npmResolution?.version,
resolvedSpec: result.npmResolution?.resolvedSpec,
integrity: result.npmResolution?.integrity,
shasum: result.npmResolution?.shasum,
resolvedAt: result.npmResolution?.resolvedAt,
});
changed = true;
@@ -406,6 +461,12 @@ export async function syncPluginsForUpdateChannel(params: {
spec,
installPath: result.targetDir,
version: result.version,
resolvedName: result.npmResolution?.name,
resolvedVersion: result.npmResolution?.version,
resolvedSpec: result.npmResolution?.resolvedSpec,
integrity: result.npmResolution?.integrity,
shasum: result.npmResolution?.shasum,
resolvedAt: result.npmResolution?.resolvedAt,
sourcePath: undefined,
});
summary.switchedToNpm.push(pluginId);