Security headers and sanitizing links
This commit is contained in:
parent
b12348548d
commit
87d39749e3
15
.gitignore
vendored
15
.gitignore
vendored
@ -2,3 +2,18 @@ node_modules
|
||||
dist
|
||||
.vite
|
||||
*.local
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self'; manifest-src 'self'; connect-src 'self' ws:; base-uri 'self'; form-action 'none';" />
|
||||
<title>BTCPay Server Directory</title>
|
||||
<meta name="description" content="The definitive list of merchants, creators, and organizations empowering the circular economy with BTCPay Server." />
|
||||
<meta property="og:title" content="BTCPay Server Directory" />
|
||||
|
||||
4
public/.well-known/security.txt
Normal file
4
public/.well-known/security.txt
Normal file
@ -0,0 +1,4 @@
|
||||
Contact: https://github.com/btcpayserver/btcpayserver/security/policy
|
||||
Expires: 2027-02-12T00:00:00.000Z
|
||||
Preferred-Languages: en
|
||||
Policy: https://github.com/btcpayserver/btcpayserver/security/policy
|
||||
@ -2,6 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'sha256-NOsINjbGOHhhnL1zwiLd/YHQjMqa6X9gVcT3uvwiOpE=';" />
|
||||
<title>BTCPay Server Directory</title>
|
||||
<script>
|
||||
// Redirect all 404s to the root for SPA support on GitHub Pages
|
||||
|
||||
@ -1,3 +1,2 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Sitemap: https://directory.btcpayserver.org/sitemap.xml
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Github, Twitter } from "lucide-react";
|
||||
import SupporterSprite from "@/components/SupporterSprite";
|
||||
import { supporters } from "@/data/supporters";
|
||||
import { safeUrl } from "@/lib/url";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
@ -13,7 +14,7 @@ export default function Footer() {
|
||||
{supporters.map((s) => (
|
||||
<a
|
||||
key={s.svgId}
|
||||
href={s.url}
|
||||
href={safeUrl(s.url)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title={s.name}
|
||||
|
||||
@ -2,6 +2,7 @@ import type { Merchant } from "@/data/categories";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { subTypeLabels, countryFlag, hostedBtcpayCountries } from "@/data/categories";
|
||||
import { safeUrl } from "@/lib/url";
|
||||
|
||||
interface MerchantCardProps {
|
||||
merchant: Merchant;
|
||||
@ -59,7 +60,7 @@ export default function MerchantCard({ merchant }: MerchantCardProps) {
|
||||
|
||||
{/* Action */}
|
||||
<a
|
||||
href={merchant.url}
|
||||
href={safeUrl(merchant.url)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="absolute inset-0 z-10 focus:outline-none focus:ring-2 focus:ring-primary/50 rounded-2xl sm:rounded-3xl"
|
||||
|
||||
@ -26,9 +26,11 @@ export function ThemeProvider({
|
||||
storageKey = "vite-ui-theme",
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
)
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
const valid: string[] = ["dark", "light", "system"];
|
||||
return stored && valid.includes(stored) ? (stored as Theme) : defaultTheme;
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement
|
||||
|
||||
15
src/lib/url.ts
Normal file
15
src/lib/url.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Validates that a URL uses a safe protocol (http: or https:).
|
||||
* Returns the URL unchanged if safe, or "#" as a safe fallback.
|
||||
* Prevents javascript:, data:, vbscript:, and other dangerous protocol URLs.
|
||||
*/
|
||||
export function safeUrl(url: string): string {
|
||||
try {
|
||||
const parsed = new URL(url, window.location.origin);
|
||||
return parsed.protocol === "https:" || parsed.protocol === "http:"
|
||||
? url
|
||||
: "#";
|
||||
} catch {
|
||||
return "#";
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import { useState, useMemo, useEffect, useCallback } from "react";
|
||||
import merchantsData from "@/data/merchants.json";
|
||||
import type { Merchant } from "@/data/categories";
|
||||
import { typeMap } from "@/data/categories";
|
||||
import { typeMap, mainTypes, merchantSubTypes, hostedBtcpayCountries } from "@/data/categories";
|
||||
import MerchantCard from "@/components/MerchantCard";
|
||||
import DirectoryFilters from "@/components/DirectoryFilters";
|
||||
import Navbar from "@/components/Navbar";
|
||||
@ -30,13 +30,24 @@ function shuffle<T>(array: T[]): T[] {
|
||||
}
|
||||
|
||||
// URL hash helpers for shareable filter state
|
||||
const validTypes = new Set<string>(mainTypes);
|
||||
const validSubs = new Set<string>([
|
||||
...merchantSubTypes,
|
||||
...Object.keys(hostedBtcpayCountries),
|
||||
]);
|
||||
|
||||
function parseHash(): { type: string; sub: string | null; q: string } {
|
||||
const params = new URLSearchParams(window.location.hash.slice(1));
|
||||
return {
|
||||
type: params.get("type") || "All",
|
||||
sub: params.get("sub") || null,
|
||||
q: params.get("q") || "",
|
||||
};
|
||||
|
||||
const rawType = params.get("type") || "All";
|
||||
const type = validTypes.has(rawType) ? rawType : "All";
|
||||
|
||||
const rawSub = params.get("sub") || null;
|
||||
const sub = rawSub && validSubs.has(rawSub) ? rawSub : null;
|
||||
|
||||
const q = params.get("q") || "";
|
||||
|
||||
return { type, sub, q };
|
||||
}
|
||||
|
||||
function updateHash(type: string, sub: string | null, q: string) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user