The ValidatePackageNames class validates that resolved URLs match package names, detecting potential name confusion attacks where a package's resolved URL doesn't match its declared name.
Creates a new package name validator instance.
/**
* Creates a new package name validator
* @param {Object} options - Validator configuration
* @param {Object} options.packages - Package object from parsed lockfile
* @throws {Error} If packages is not provided or not an object
*/
class ValidatePackageNames {
constructor(options: {
packages: Object;
});
}Requirements: The packages parameter must be an object (typically from ParseLockfile.parseSync().object). Throws an error if not provided or not an object.
Usage Examples:
const { ValidatePackageNames, ParseLockfile } = require('lockfile-lint-api');
// Parse lockfile first
const parser = new ParseLockfile({ lockfilePath: './package-lock.json' });
const lockfile = parser.parseSync();
// Create validator
const validator = new ValidatePackageNames({ packages: lockfile.object });Validates that all packages' resolved URLs match their declared names.
/**
* Validates package name consistency between declared name and resolved URL
* @param {string[]} [packageNameAliases] - Optional array of 'name:alias' strings for legitimate aliases
* @returns {Object} Validation result with type ('success' or 'error') and errors array
*/
validate(packageNameAliases?: string[]): {
type: 'success' | 'error';
errors: Array<{
message: string;
package: string;
}>;
}Behavior:
resolved field are automatically skipped (e.g., local filesystem packages)Package Name Aliases:
The optional packageNameAliases parameter allows specifying legitimate package name aliases in the format 'declared-name:actual-name'. This is useful for packages that intentionally use different names in the lockfile versus the registry.
Usage Examples:
const { ValidatePackageNames, ParseLockfile } = require('lockfile-lint-api');
// Basic usage
const parser = new ParseLockfile({ lockfilePath: './package-lock.json' });
const lockfile = parser.parseSync();
const validator = new ValidatePackageNames({ packages: lockfile.object });
const result = validator.validate();
if (result.type === 'success') {
console.log('All package names are consistent');
} else {
console.error('Package name validation failed:');
result.errors.forEach(error => {
console.error(` ${error.package}: ${error.message}`);
});
}Example with Package Aliases:
const { ValidatePackageNames, ParseLockfile } = require('lockfile-lint-api');
const parser = new ParseLockfile({ lockfilePath: './package-lock.json' });
const lockfile = parser.parseSync();
const validator = new ValidatePackageNames({ packages: lockfile.object });
// Allow specific package name aliases
const result = validator.validate([
'my-package-alias:actual-package-name',
'another-alias:real-package'
]);
if (result.type === 'error') {
console.error('Name confusion detected:');
result.errors.forEach(error => console.error(error.message));
}CI/CD Integration Example:
const { ValidatePackageNames, ParseLockfile } = require('lockfile-lint-api');
function detectNameConfusionAttacks(lockfilePath) {
try {
const parser = new ParseLockfile({ lockfilePath });
const lockfile = parser.parseSync();
const validator = new ValidatePackageNames({ packages: lockfile.object });
const result = validator.validate();
if (result.type === 'error') {
console.error('SECURITY ALERT: Package name confusion detected!');
result.errors.forEach(error => {
console.error(` Package: ${error.package}`);
console.error(` ${error.message}`);
});
return false;
}
console.log('✓ All package names are consistent');
return true;
} catch (error) {
console.error('Validation error:', error.message);
return false;
}
}
if (!detectNameConfusionAttacks('./package-lock.json')) {
console.error('Build failed due to security concerns');
process.exit(1);
}The validate() method returns a consistent result object:
interface PackageNamesValidationResult {
type: 'success' | 'error';
errors: PackageNamesValidationError[];
}
interface PackageNamesValidationError {
message: string; // Detailed error message with expected and actual package names
package: string; // Package name that failed validation
}Success Example:
{
type: 'success',
errors: []
}Error Example:
{
type: 'error',
errors: [
{
message: 'detected resolved URL for package with a different name: lodash\n expected: lodash\n actual: malicious-lodash\n',
package: 'lodash'
}
]
}Package name validation prevents name confusion attacks:
Name Confusion Attack Example:
A malicious actor could create a lockfile where:
lodash@4.17.21https://registry.npmjs.org/malicious-lodash/-/malicious-lodash-4.17.21.tgzThis validator would detect this mismatch and flag it as an error.
The validator only checks packages from official public registries:
registry.npmjs.org (npm registry)registry.yarnpkg.com (Yarn registry)registry.verdaccio.org (Verdaccio registry)Packages from other registries (private, corporate, custom, etc.) are automatically skipped because:
Example:
// These will be validated
resolved: 'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz'
resolved: 'https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz'
// These will be skipped
resolved: 'https://registry.mycompany.com/lodash/-/lodash-4.17.21.tgz'
resolved: 'https://nexus.example.org/lodash/-/lodash-4.17.21.tgz'The validator handles scoped packages correctly:
// Regular packages
'lodash@4.17.21' → 'lodash'
'express@4.18.0' → 'express'
// Scoped packages
'@babel/core@7.20.0' → '@babel/core'
'@types/node@18.0.0' → '@types/node'The validator expects the following URL structure for official registries:
https://<registry-host>/<package-name>/-/<package-name>-<version>.tgzIt extracts the package name from the URL path and compares it to the declared package name.
The packageNameAliases parameter is useful for edge cases where packages legitimately use different names:
Format: 'declared-name:actual-name'
Example Scenarios:
const validator = new ValidatePackageNames({ packages: lockfile.object });
// Allow a package that's aliased in package.json
// "dependencies": { "my-lodash": "npm:lodash@4.17.21" }
const result = validator.validate(['my-lodash:lodash']);
// Allow multiple aliases
const result = validator.validate([
'my-lodash:lodash',
'my-react:react',
'company-utils:@company/utils'
]);Alias Processing:
When an alias is provided:
:)Constructor Errors:
// Throws: 'expecting an object passed to validator constructor'
const validator = new ValidatePackageNames(); // Missing packages
const validator = new ValidatePackageNames({ packages: null }); // Invalid type
const validator = new ValidatePackageNames({ packages: [] }); // Array instead of objectComprehensive Security Check:
const {
ParseLockfile,
ValidateHost,
ValidateHttps,
ValidatePackageNames
} = require('lockfile-lint-api');
function comprehensiveSecurityAudit(lockfilePath) {
const parser = new ParseLockfile({ lockfilePath });
const lockfile = parser.parseSync();
const validators = [
{
name: 'HTTPS Protocol',
validator: new ValidateHttps({ packages: lockfile.object }),
args: []
},
{
name: 'Allowed Hosts',
validator: new ValidateHost({ packages: lockfile.object }),
args: [['npm']]
},
{
name: 'Package Names',
validator: new ValidatePackageNames({ packages: lockfile.object }),
args: [[]]
}
];
let allPassed = true;
validators.forEach(({ name, validator, args }) => {
const result = validator.validate(...args);
if (result.type === 'error') {
console.error(`\n${name} validation failed:`);
result.errors.forEach(error => console.error(error.message));
allPassed = false;
} else {
console.log(`✓ ${name} validation passed`);
}
});
return allPassed;
}
if (!comprehensiveSecurityAudit('./package-lock.json')) {
console.error('\nSecurity audit failed!');
process.exit(1);
}The validator uses the debug package for internal logging. Enable debug output:
DEBUG=lockfile-lint node your-script.jsDebug messages include:
Example Debug Output:
lockfile-lint skipping package name validation for aliased package name: my-lodash@4.17.21 resolving to: lodash
lockfile-lint skipping package name 'react@18.2.0' validation for non-official registry 'https://registry.mycompany.com'