Skip to content
Open
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
3 changes: 2 additions & 1 deletion scripts/import-architectures.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { execFileSync } from 'node:child_process';
import { join, relative } from 'node:path';
import { tmpdir } from 'node:os';
import { parse as yamlParse } from 'yaml';
import { isCncfProjectHref } from './lib/project-card-links.mjs';

const root = new URL('..', import.meta.url).pathname;
const upstream = mkdtempSync(join(tmpdir(), 'cncf-architecture-'));
Expand Down Expand Up @@ -135,7 +136,7 @@ function renderProjectCards(body, id) {
(match) => match[1],
);
const href =
links.find((link) => link.includes('cncf.io/projects/')) ||
links.find(isCncfProjectHref) ||
`https://www.cncf.io/projects/${name.toLowerCase().replace(/\s+/g, '-')}/`;
const logo = (content.match(/!\[[^\]]*\]\((https?:\/\/[^)]+)\)/) ||
[])[1];
Expand Down
34 changes: 34 additions & 0 deletions scripts/lib/project-card-links.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Link validation for imported CNCF project cards.
//
// Card link targets come from Markdown in https://github.com/cncf/architecture,
// which is cloned and imported unattended by the daily import workflow. A
// substring test such as `link.includes('cncf.io/projects/')` is satisfied by
// any host that merely mentions that text in a path, query or fragment, so the
// destination must be decided by parsing the URL rather than by matching text.

/**
* True only for an https URL whose host is cncf.io (or a subdomain of it) and
* whose path is under /projects/.
*
* @param {string} value
* @returns {boolean}
*/
export function isCncfProjectHref(value) {
if (typeof value !== 'string') return false;

let url;
try {
url = new URL(value);
} catch {
return false;
}

if (url.protocol !== 'https:') return false;
// Userinfo lets "https://www.cncf.io@evil.example/projects/x" read as CNCF.
if (url.username || url.password) return false;

const host = url.hostname.toLowerCase();
if (host !== 'cncf.io' && !host.endsWith('.cncf.io')) return false;

return url.pathname === '/projects' || url.pathname.startsWith('/projects/');
}
73 changes: 73 additions & 0 deletions tests/project-card-links.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import test from 'node:test';
import assert from 'node:assert/strict';

import { isCncfProjectHref } from '../scripts/lib/project-card-links.mjs';

test('accepts canonical CNCF project URLs', () => {
assert.equal(
isCncfProjectHref('https://www.cncf.io/projects/kubernetes/'),
true,
);
assert.equal(isCncfProjectHref('https://cncf.io/projects/helm/'), true);
assert.equal(isCncfProjectHref('https://www.cncf.io/projects'), true);
assert.equal(
isCncfProjectHref('https://www.cncf.io/projects/argo/?ref=architecture'),
true,
);
});

test('rejects a foreign host that merely mentions the CNCF project path', () => {
assert.equal(
isCncfProjectHref('https://evil.example/r?u=cncf.io/projects/kubernetes/'),
false,
);
assert.equal(
isCncfProjectHref('https://evil.example/cncf.io/projects/kubernetes/'),
false,
);
assert.equal(
isCncfProjectHref('https://evil.example/#cncf.io/projects/kubernetes/'),
false,
);
});

test('rejects lookalike hosts', () => {
assert.equal(
isCncfProjectHref('https://cncf.io.evil.example/projects/x'),
false,
);
assert.equal(isCncfProjectHref('https://notcncf.io/projects/x'), false);
});

test('rejects userinfo that disguises the real host', () => {
assert.equal(
isCncfProjectHref('https://www.cncf.io@evil.example/projects/x'),
false,
);
});

test('rejects non-https schemes', () => {
assert.equal(
isCncfProjectHref('http://www.cncf.io/projects/kubernetes/'),
false,
);
assert.equal(
isCncfProjectHref('javascript:alert(1)//cncf.io/projects/x'),
false,
);
});

test('rejects CNCF URLs outside /projects/', () => {
assert.equal(isCncfProjectHref('https://www.cncf.io/blog/projects/x'), false);
assert.equal(
isCncfProjectHref('https://www.cncf.io/projectsomething'),
false,
);
});

test('rejects non-URL and non-string input', () => {
assert.equal(isCncfProjectHref('not a url'), false);
assert.equal(isCncfProjectHref(''), false);
assert.equal(isCncfProjectHref(undefined), false);
assert.equal(isCncfProjectHref(null), false);
});
Loading