Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions scripts/docs-semantic-core.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1212,13 +1212,27 @@ function describeValue(value) {
}

function normalizeHeadingText(value) {
return value
return stripHeadingTags(value)
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
.replace(/<[^>]*>/g, '')
.replace(/[`*_~]/g, '')
.trim();
}

function stripHeadingTags(value) {
let result = '';
let insideTag = false;
for (const character of value) {
if (character === '<') {
insideTag = true;
} else if (character === '>') {
insideTag = false;
} else if (!insideTag) {
result += character;
}
}
return result;
}

function slugifyHeading(value) {
return value
.toLowerCase()
Expand Down
43 changes: 37 additions & 6 deletions scripts/release-artifact-core.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,14 @@ export function normalizeNpmViewPublicationState(value) {
}

export function inspectNpmAttestations(value, { packageName, version }) {
const expectedSuffix = `${encodeURIComponent(packageName).replace('%40', '@')}@${version}`;
const packageNamePattern = /^(?:@[a-z0-9][a-z0-9._~-]*\/[a-z0-9][a-z0-9._~-]*|[a-z0-9][a-z0-9._~-]*)$/;
const validPackageName =
typeof packageName === 'string' &&
packageName.length <= 214 &&
packageNamePattern.test(packageName);
const validVersion = typeof version === 'string' && /^\d+\.\d+\.\d+$/.test(version);
const expectedIdentity =
validPackageName && validVersion ? `${packageName}@${version}` : null;
const entries = Array.isArray(value) ? value : [value];
const attestation = entries.length === 1 ? entries[0] : null;
const url = attestation?.url;
Expand All @@ -208,11 +215,10 @@ export function inspectNpmAttestations(value, { packageName, version }) {
if (entries.length !== 1) {
errors.push(`npm view must return exactly one attestation, received ${entries.length}`);
}
if (
typeof url !== 'string' ||
!url.startsWith('https://registry.npmjs.org/-/npm/v1/attestations/') ||
!decodeURIComponent(url).endsWith(expectedSuffix)
) {
if (!validPackageName || !validVersion) {
errors.push('npm attestation identity must use an exact package name and semantic version');
}
if (!isExactNpmAttestationUrl(url, expectedIdentity)) {
errors.push('npm attestation URL does not identify the exact package version');
}
if (predicateType !== 'https://slsa.dev/provenance/v1') {
Expand All @@ -226,6 +232,31 @@ export function inspectNpmAttestations(value, { packageName, version }) {
};
}

function isExactNpmAttestationUrl(value, expectedIdentity) {
if (typeof value !== 'string' || expectedIdentity == null) {
return false;
}
try {
const parsed = new URL(value);
const pathPrefix = '/-/npm/v1/attestations/';
if (
parsed.protocol !== 'https:' ||
parsed.hostname !== 'registry.npmjs.org' ||
parsed.port !== '' ||
parsed.username !== '' ||
parsed.password !== '' ||
parsed.search !== '' ||
parsed.hash !== '' ||
!parsed.pathname.startsWith(pathPrefix)
) {
return false;
}
return decodeURIComponent(parsed.pathname.slice(pathPrefix.length)) === expectedIdentity;
} catch {
return false;
}
}

function publicationFailure(
version,
artifactIntegrity,
Expand Down
14 changes: 14 additions & 0 deletions test/docsSemantic.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -494,4 +494,18 @@ describe('documentation semantic gate', () => {
{ text: 'A.B', anchor: 'ab-1' },
]);
});

it('removes nested heading tags without producing a new tag-shaped substring', () => {
const headings = parseHeadings(
'# <code>API</code>\n\n## <<script>alert</script>\n\n## Before <em>and</em> after\n\n## Unclosed <script\n\n## <scr<script>ipt>'
);
expect(headings).toEqual([
{ text: 'API', anchor: 'api' },
{ text: 'alert', anchor: 'alert' },
{ text: 'Before and after', anchor: 'before-and-after' },
{ text: 'Unclosed', anchor: 'unclosed' },
{ text: 'ipt', anchor: 'ipt' },
]);
expect(headings.every(({ text }) => !/[<>]/u.test(text))).toBe(true);
});
});
45 changes: 45 additions & 0 deletions test/releaseArtifact.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,49 @@ describe('release artifact contract', () => {
error: expect.stringContaining('exactly one attestation'),
});
});

it('parses the exact npm attestation URL identity without substring sanitization', () => {
const provenance = { predicateType: 'https://slsa.dev/provenance/v1' };
expect(
inspectNpmAttestations(
{
url: 'https://registry.npmjs.org/-/npm/v1/attestations/%40scope%2Fpackage@1.2.3',
provenance,
},
{ packageName: '@scope/package', version: '1.2.3' }
)
).toMatchObject({ status: 'passed' });

for (const url of [
'https://registry.npmjs.org/-/npm/v1/attestations/evil-package@1.2.3',
'https://registry.npmjs.org/-/npm/v1/attestations/package@1.2.3?identity=package@1.2.3',
'https://registry.npmjs.org/-/npm/v1/attestations/package@1.2.3#package@1.2.3',
'https://registry.npmjs.org.evil.example/-/npm/v1/attestations/package@1.2.3',
'https://package@registry.npmjs.org/-/npm/v1/attestations/package@1.2.3',
'https://registry.npmjs.org/-/npm/v1/attestations/%E0%A4%A',
]) {
expect(
inspectNpmAttestations(
{ url, provenance },
{ packageName: 'package', version: '1.2.3' }
)
).toMatchObject({
status: 'failed',
error: expect.stringContaining('exact package version'),
});
}

expect(
inspectNpmAttestations(
{
url: 'https://registry.npmjs.org/-/npm/v1/attestations/%2540scope%252Fpackage@1.2.3',
provenance,
},
{ packageName: '@scope/package', version: '1.2.3' }
)
).toMatchObject({
status: 'failed',
error: expect.stringContaining('exact package version'),
});
});
});