| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import fs from 'node:fs'; | ||
| import path from 'node:path'; | ||
| import { hasLockedIntegrity } from './subresourceIntegrity.mjs'; | ||
|
|
||
| const excludedDirectoryNames = new Set(['.git', 'node_modules']); | ||
|
|
||
| function findPackageLockPaths(repositoryRoot) { | ||
| const packageLockPaths = []; | ||
|
|
||
| function visit(directory) { | ||
| const entries = fs.readdirSync(directory, { withFileTypes: true }) | ||
| .sort((left, right) => left.name.localeCompare(right.name)); | ||
| for (const entry of entries) { | ||
| const entryPath = path.join(directory, entry.name); | ||
| if (entry.isDirectory() && !excludedDirectoryNames.has(entry.name)) { | ||
| visit(entryPath); | ||
| } else if (entry.isFile() && entry.name === 'package-lock.json') { | ||
| packageLockPaths.push(entryPath); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| visit(repositoryRoot); | ||
| return packageLockPaths; | ||
| } | ||
|
|
||
| function getWorkspaceTargetPath(packageEntry) { | ||
| if (packageEntry.link !== true || typeof packageEntry.resolved !== 'string') { | ||
| return undefined; | ||
| } | ||
|
|
||
| const workspacePath = packageEntry.resolved.replaceAll('\\', '/'); | ||
| const normalizedPath = path.posix.normalize(workspacePath); | ||
| return workspacePath.length > 0 | ||
| && !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(workspacePath) | ||
| && !path.posix.isAbsolute(workspacePath) | ||
| && normalizedPath === workspacePath | ||
| && normalizedPath !== '.' | ||
| && normalizedPath !== '..' | ||
| && !normalizedPath.startsWith('../') | ||
| ? normalizedPath | ||
| : undefined; | ||
| } | ||
|
|
||
| function getWorkspacePaths(packages) { | ||
| return new Set(Object.values(packages) | ||
| .map(getWorkspaceTargetPath) | ||
| .filter(workspacePath => workspacePath !== undefined) | ||
| .filter(workspacePath => { | ||
| const workspaceEntry = packages[workspacePath]; | ||
| return !workspacePath.split('/').includes('node_modules') | ||
| && workspaceEntry !== undefined | ||
| && workspaceEntry.resolved === undefined | ||
| && workspaceEntry.integrity === undefined; | ||
| })); | ||
| } | ||
|
|
||
| function isBundledPackageEntry(packages, packagePath, packageEntry) { | ||
| if (packageEntry.inBundle !== true || !packagePath.includes('/node_modules/')) { | ||
| return false; | ||
| } | ||
|
|
||
| let ancestorPath = packagePath.slice(0, packagePath.lastIndexOf('/node_modules/')); | ||
| while (ancestorPath) { | ||
| const ancestorEntry = packages[ancestorPath]; | ||
| if (ancestorEntry && hasLockedIntegrity(ancestorEntry.integrity)) { | ||
| const relativePath = packagePath.slice(`${ancestorPath}/node_modules/`.length); | ||
| const relativeSegments = relativePath.split('/'); | ||
| const packageName = relativeSegments[0].startsWith('@') | ||
| ? relativeSegments.slice(0, 2).join('/') | ||
| : relativeSegments[0]; | ||
| return Array.isArray(ancestorEntry.bundleDependencies) && ancestorEntry.bundleDependencies.includes(packageName); | ||
| } | ||
|
|
||
| const nextSeparator = ancestorPath.lastIndexOf('/node_modules/'); | ||
| if (nextSeparator === -1) { | ||
| break; | ||
| } | ||
| ancestorPath = ancestorPath.slice(0, nextSeparator); | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| function isExplicitLocalPackageEntry(packages, workspacePaths, packagePath, packageEntry) { | ||
| const normalizedPackagePath = packagePath.replaceAll('\\', '/'); | ||
| return workspacePaths.has(getWorkspaceTargetPath(packageEntry)) | ||
| || workspacePaths.has(normalizedPackagePath) | ||
| || (packageEntry.link !== true && typeof packageEntry.resolved === 'string' && /^(?:file|link|workspace):/i.test(packageEntry.resolved)) | ||
| || isBundledPackageEntry(packages, normalizedPackagePath, packageEntry); | ||
| } | ||
|
|
||
| export { findPackageLockPaths, getWorkspacePaths, isExplicitLocalPackageEntry }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import { Buffer } from 'node:buffer'; | ||
| import { createHash } from 'node:crypto'; | ||
|
|
||
| const digestLengths = new Map([ | ||
| ['sha1', 20], | ||
| ['sha256', 32], | ||
| ['sha384', 48], | ||
| ['sha512', 64] | ||
| ]); | ||
| const supportedAlgorithms = new Set(['sha256', 'sha384', 'sha512']); | ||
|
|
||
| function calculateIntegrity(algorithm, content) { | ||
| return `${algorithm}-${createHash(algorithm).update(content).digest('base64')}`; | ||
| } | ||
|
|
||
| function parseValidDigest(digest) { | ||
| const metadataSeparator = digest.indexOf('?'); | ||
| const digestWithoutMetadata = metadataSeparator === -1 ? digest : digest.slice(0, metadataSeparator); | ||
| const match = /^(sha1|sha256|sha384|sha512)-([A-Za-z0-9+/]+={0,2})$/.exec(digestWithoutMetadata); | ||
| if (!match) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const [, algorithm, serializedDigest] = match; | ||
| const decodedDigest = Buffer.from(serializedDigest, 'base64'); | ||
| return decodedDigest.length === digestLengths.get(algorithm) | ||
| && decodedDigest.toString('base64').replace(/=+$/, '') === serializedDigest.replace(/=+$/, '') | ||
| ? { algorithm, digest: decodedDigest } | ||
| : undefined; | ||
| } | ||
|
|
||
| function hasLockedIntegrity(integrity) { | ||
| return typeof integrity === 'string' && integrity.split(/\s+/).some(digest => parseValidDigest(digest) !== undefined); | ||
| } | ||
|
|
||
| function hasSupportedIntegrityAlgorithm(integrity) { | ||
| return typeof integrity === 'string' && integrity.split(/\s+/).some(digest => supportedAlgorithms.has(parseValidDigest(digest)?.algorithm)); | ||
| } | ||
|
|
||
| function integrityMatchesContent(integrity, algorithm, content) { | ||
| if (typeof integrity !== 'string') { | ||
| return false; | ||
| } | ||
|
|
||
| const expectedDigest = createHash(algorithm).update(content).digest(); | ||
| return integrity.split(/\s+/).some(serializedDigest => { | ||
| const parsedDigest = parseValidDigest(serializedDigest); | ||
| return parsedDigest?.algorithm === algorithm && parsedDigest.digest.equals(expectedDigest); | ||
| }); | ||
| } | ||
|
|
||
| export { calculateIntegrity, hasLockedIntegrity, hasSupportedIntegrityAlgorithm, integrityMatchesContent }; |
| Back | FazBrowse Home | New Git URL |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality✨Copilot (agent102): [Minor] This exemption treats a lockfile-controlled resolved prefix as authorization to skip integrity, without validating the target — and the same pattern exists on the Yarn side at verifyYarnLock.mjs line 93 (!isExplicitLocalResolution(currentEntry.resolved)). Both were raised as suppressed low-confidence observations in the automated review at this head; I verified them against this revision rather than taking them as given.
Calling the exported validators directly at 62cd4633:
So the link: true path that was hardened earlier in this PR rejects traversal, absolute, and protocol targets, while its non-link sibling accepts an arbitrary string after the protocol prefix. On the Yarn side an ordinary registry selector is exempted purely because of its resolved line, which does not match the earlier statement that "only selectors whose actual dependency range uses an explicit file:, link:, or workspace: protocol are exempt" — the condition is an OR of the selector check and the resolution check.
Impact is bounded: no entry in any current lockfile lacks integrity, so nothing relies on this exemption today, and it only matters for a future lockfile change. But since the stated goal is repository-wide integrity enforcement, it is worth closing so the validator cannot be opted out of by editing the lockfile it is validating.
Minimal resolution: apply the same target rules the link: true path already uses — resolve the local target relative to the lockfile and exempt only normalized, repository-contained paths — or, for Yarn, rely on hasExplicitLocalSelector alone. Because every current entry carries integrity, either change is inert against the lockfiles in this PR.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.