Skip to content

Commit be32219

Browse files
authored
Removes untrusted documentation links and implements URL security validation (#73)
Replaces references to the docs.paseo.network domain with GitHub alternatives across the site. Introduces a new security test suite to enforce a domain allowlist, prevent the use of blocked phishing domains, and ensure all URLs use secure protocols (HTTPS/WSS).
1 parent 8a8318f commit be32219

5 files changed

Lines changed: 144 additions & 11 deletions

File tree

src/components/sections/UsersSection.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,11 @@ export function UsersSection() {
6767
{PASEO_USERS.map((user) => {
6868
const hasLogo = "logo" in user && user.logo;
6969
const hasDarkLogo = "logoDark" in user && user.logoDark;
70-
const shouldInvert =
71-
"darkInvert" in user && user.darkInvert;
70+
const shouldInvert = "darkInvert" in user && user.darkInvert;
7271
const isWordmark = "isWordmark" in user && user.isWordmark;
7372

7473
const invertClass = shouldInvert ? "dark:invert" : "";
75-
const logoClass =
76-
`object-contain opacity-70 group-hover:opacity-100 transition-opacity duration-300 ${invertClass}`;
74+
const logoClass = `object-contain opacity-70 group-hover:opacity-100 transition-opacity duration-300 ${invertClass}`;
7775

7876
const content = isWordmark ? (
7977
<UserLogo

src/components/sections/developers/DevelopersHeroSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function DevelopersHeroSection() {
3636
</PrimaryButton>
3737
<SecondaryButton
3838
className="group"
39-
onClick={() => window.open(URLS.docs, "_blank")}
39+
onClick={() => window.open(URLS.github, "_blank")}
4040
>
4141
<FileText className="mr-2 h-4 w-4" />
4242
Documentation
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { describe, expect, it } from "vitest";
2+
import { URLS } from "../urls";
3+
4+
/**
5+
* Allowlist of trusted domains for all URLs in the project.
6+
* Any new domain must be explicitly added here after review.
7+
*/
8+
const ALLOWED_DOMAINS = [
9+
"github.com",
10+
"paseo-r2.zondax.ch",
11+
"validators.paseo.site",
12+
"grafana.paseo.site",
13+
"hub.regionx.tech",
14+
"x.com",
15+
"matrix.to",
16+
"polkadot.com",
17+
"wiki.polkadot.com",
18+
"polkadot-fellows.github.io",
19+
"zondax.ch",
20+
"faucet.polkadot.io",
21+
"onpop.io",
22+
"www.deploypolkadot.xyz",
23+
"docs.polkadot.com",
24+
"blockscout-passet-hub.parity-testnet.parity.io",
25+
"paseo.subscan.io",
26+
"polkadot.testnet.routescan.io",
27+
"testnet-passet-hub-eth-rpc.polkadot.io",
28+
"passet-hub-paseo.ibp.network",
29+
];
30+
31+
/**
32+
* Known phishing or compromised domains that must never appear in the codebase.
33+
*/
34+
const BLOCKED_DOMAINS = ["docs.paseo.network"];
35+
36+
function extractAllUrls(
37+
obj: unknown,
38+
path = "",
39+
): { url: string; path: string }[] {
40+
const urls: { url: string; path: string }[] = [];
41+
42+
if (typeof obj === "string") {
43+
if (
44+
obj.startsWith("http://") ||
45+
obj.startsWith("https://") ||
46+
obj.startsWith("wss://")
47+
) {
48+
urls.push({ url: obj, path });
49+
}
50+
} else if (typeof obj === "object" && obj !== null) {
51+
for (const [key, value] of Object.entries(obj)) {
52+
urls.push(...extractAllUrls(value, path ? `${path}.${key}` : key));
53+
}
54+
}
55+
56+
return urls;
57+
}
58+
59+
function getDomain(url: string): string {
60+
try {
61+
return new URL(url.replace("wss://", "https://")).hostname;
62+
} catch {
63+
return "";
64+
}
65+
}
66+
67+
describe("URL Security", () => {
68+
const allUrls = extractAllUrls(URLS);
69+
70+
it("should have URLs to validate", () => {
71+
expect(allUrls.length).toBeGreaterThan(0);
72+
});
73+
74+
it("should only contain URLs from allowed domains", () => {
75+
const disallowed = allUrls.filter(({ url }) => {
76+
const domain = getDomain(url);
77+
return !ALLOWED_DOMAINS.some(
78+
(allowed) => domain === allowed || domain.endsWith(`.${allowed}`),
79+
);
80+
});
81+
82+
if (disallowed.length > 0) {
83+
const details = disallowed
84+
.map(({ url, path }) => ` ${path}: ${url}`)
85+
.join("\n");
86+
throw new Error(
87+
`Found URLs with domains not in the allowlist:\n${details}\n\n` +
88+
"If this domain is legitimate, add it to ALLOWED_DOMAINS in urls-security.test.ts",
89+
);
90+
}
91+
});
92+
93+
it("should not contain any blocked/phishing domains", () => {
94+
const blocked = allUrls.filter(({ url }) => {
95+
const domain = getDomain(url);
96+
return BLOCKED_DOMAINS.some(
97+
(b) => domain === b || domain.endsWith(`.${b}`),
98+
);
99+
});
100+
101+
if (blocked.length > 0) {
102+
const details = blocked
103+
.map(({ url, path }) => ` ${path}: ${url}`)
104+
.join("\n");
105+
throw new Error(`SECURITY: Found blocked/phishing domains:\n${details}`);
106+
}
107+
});
108+
109+
it("should only use HTTPS or WSS protocols", () => {
110+
const insecure = allUrls.filter(
111+
({ url }) => !url.startsWith("https://") && !url.startsWith("wss://"),
112+
);
113+
114+
if (insecure.length > 0) {
115+
const details = insecure
116+
.map(({ url, path }) => ` ${path}: ${url}`)
117+
.join("\n");
118+
throw new Error(`Found URLs using insecure protocols:\n${details}`);
119+
}
120+
});
121+
122+
it("should have valid URL format", () => {
123+
const invalid = allUrls.filter(({ url }) => {
124+
try {
125+
new URL(url.replace("wss://", "https://"));
126+
return false;
127+
} catch {
128+
return true;
129+
}
130+
});
131+
132+
if (invalid.length > 0) {
133+
const details = invalid
134+
.map(({ url, path }) => ` ${path}: ${url}`)
135+
.join("\n");
136+
throw new Error(`Found malformed URLs:\n${details}`);
137+
}
138+
});
139+
});

src/constants/content.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const HERO_CONTENT = {
2828
},
2929
secondary: {
3030
label: "Documentation",
31-
href: URLS.docs,
31+
href: URLS.github,
3232
},
3333
},
3434
} as const satisfies HeroContent;
@@ -210,7 +210,7 @@ export const COMMUNITY_CONTENT = {
210210
},
211211
secondary: {
212212
label: "View PAS Documents",
213-
href: URLS.docsGovernance,
213+
href: URLS.githubGovernance,
214214
},
215215
},
216216
},

src/constants/urls.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ export const URLS = {
55
grafanaLogs: "https://grafana.paseo.site/",
66
regionXHub: "https://hub.regionx.tech/?network=paseo",
77

8-
// Documentation
9-
docs: "https://docs.paseo.network",
10-
docsGovernance: "https://docs.paseo.network/governance",
11-
128
// GitHub - Paseo Network
139
github: "https://github.com/paseo-network",
1410
githubChainSpecs: "https://github.com/paseo-network/paseo-chain-specs",

0 commit comments

Comments
 (0)