Skip to content

Commit 8e96524

Browse files
tapiwamakandigonaViktor
andauthored
Add a hash-based Content-Security-Policy to every page (#40)
tapiwa.me sent no CSP at all. Appwrite Sites' static adapter has no custom-header support, so the policy ships as a <meta http-equiv> stamped onto all 22 pages by build_site.py. script-src is hash-based, not 'unsafe-inline'. Eight pages carry inline scripts, so each page gets the sha256 of its own inline blocks; a blanket 'unsafe-inline' would have made the policy decorative. JSON-LD blocks are not executable and browsers do not treat them as script for CSP purposes, so they are deliberately not hashed. Everything else is same-origin by default, with narrow exceptions for what the site actually uses: Google Fonts, the consent-gated Google tag, api.github.com (the live release list on /lanlink/downloads/), and play.google.com for the Play Store badge. Verified in a browser, not assumed: 27/27 checks across all 22 pages, with zero CSP violations and zero page errors. The harness confirms the WebGL hero still renders, the consent panel still appears, webfonts still load, and the hashed inline script on the downloads page still executes. It also recomputes every page's inline-script hashes and asserts each one is present in that page's own policy, so a stale or half-stamped build fails the check rather than shipping silently. Two defects were caught this way before shipping: 1. img-src omitted play.google.com, which blocked the "Get it on Google Play" badge on the app pages. 2. The redirect stubs under /blog/, /about/, /contact/, /home/ and /projects/ needed grading from their own markup — following their meta-refresh graded news.tapiwa.me instead. Known limitation: frame-ancestors is ignored when a policy is delivered by meta tag, so clickjacking protection still needs a real response header. That requires either an SSR adapter or a proxy in front of the static site and is deliberately left as a separate decision. Co-authored-by: Viktor <viktor@tapiwa.me>
1 parent 2b7ebb0 commit 8e96524

22 files changed

Lines changed: 46 additions & 2 deletions

File tree

404.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4+
<!-- Content-Security-Policy (see build_site.py) -->
5+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: https://play.google.com https://www.google-analytics.com https://www.googletagmanager.com; media-src 'self'; connect-src 'self' https://api.github.com https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com https://www.googletagmanager.com; worker-src 'self' blob:; manifest-src 'self'; frame-src 'none'; upgrade-insecure-requests" />
46
<meta charset="UTF-8" />
57
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
68
<title>404 — Page not found · Tapiwa Makandigona</title>

about/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<!doctype html>
22
<html lang="en">
33
<head>
4+
<!-- Content-Security-Policy (see build_site.py) -->
5+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: https://play.google.com https://www.google-analytics.com https://www.googletagmanager.com; media-src 'self'; connect-src 'self' https://api.github.com https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com https://www.googletagmanager.com; worker-src 'self' blob:; manifest-src 'self'; frame-src 'none'; upgrade-insecure-requests" />
46
<meta charset="utf-8" />
57
<title>Redirecting…</title>
68
<meta http-equiv="refresh" content="0; url=/" />

blog/3d-fps-game-in-the-browser/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4+
<!-- Content-Security-Policy (see build_site.py) -->
5+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; script-src 'self' https://www.googletagmanager.com 'sha256-M9Ktx5fsO0cPQcYBDZ4vrcCqzeVU+7xVsYEZ80w/4Qg='; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: https://play.google.com https://www.google-analytics.com https://www.googletagmanager.com; media-src 'self'; connect-src 'self' https://api.github.com https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com https://www.googletagmanager.com; worker-src 'self' blob:; manifest-src 'self'; frame-src 'none'; upgrade-insecure-requests" />
46
<meta charset="UTF-8" />
57
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
68
<title>Build a 3D FPS Game in the Browser with Three.js &amp; TypeScript (2026) | Tapiwa Makandigona</title>

blog/banking-app-with-supabase/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4+
<!-- Content-Security-Policy (see build_site.py) -->
5+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; script-src 'self' https://www.googletagmanager.com 'sha256-XtR//L26nkbWEhAnnwQosTw+tKBYXdW6uJ2jgDPpSRg='; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: https://play.google.com https://www.google-analytics.com https://www.googletagmanager.com; media-src 'self'; connect-src 'self' https://api.github.com https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com https://www.googletagmanager.com; worker-src 'self' blob:; manifest-src 'self'; frame-src 'none'; upgrade-insecure-requests" />
46
<meta charset="UTF-8" />
57
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
68
<title>Building a banking app with React and Supabase Row Level Security | Tapiwa Makandigona</title>

blog/building-a-ride-hailing-app/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4+
<!-- Content-Security-Policy (see build_site.py) -->
5+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; script-src 'self' https://www.googletagmanager.com 'sha256-XtR//L26nkbWEhAnnwQosTw+tKBYXdW6uJ2jgDPpSRg='; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: https://play.google.com https://www.google-analytics.com https://www.googletagmanager.com; media-src 'self'; connect-src 'self' https://api.github.com https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com https://www.googletagmanager.com; worker-src 'self' blob:; manifest-src 'self'; frame-src 'none'; upgrade-insecure-requests" />
46
<meta charset="UTF-8" />
57
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
68
<title>How I built a ride-hailing app without a Google Maps API key | Tapiwa Makandigona</title>

blog/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4+
<!-- Content-Security-Policy (see build_site.py) -->
5+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; script-src 'self' https://www.googletagmanager.com 'sha256-XtR//L26nkbWEhAnnwQosTw+tKBYXdW6uJ2jgDPpSRg='; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: https://play.google.com https://www.google-analytics.com https://www.googletagmanager.com; media-src 'self'; connect-src 'self' https://api.github.com https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com https://www.googletagmanager.com; worker-src 'self' blob:; manifest-src 'self'; frame-src 'none'; upgrade-insecure-requests" />
46
<meta charset="UTF-8" />
57
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
68
<title>Build notes have moved to news.tapiwa.me | Tapiwa Makandigona</title>

blog/provably-fair-casino-games/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4+
<!-- Content-Security-Policy (see build_site.py) -->
5+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; script-src 'self' https://www.googletagmanager.com 'sha256-oltFZl342eCE8vdgoDikpCyl4eBalLt5pYKhF2ncCiA='; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: https://play.google.com https://www.google-analytics.com https://www.googletagmanager.com; media-src 'self'; connect-src 'self' https://api.github.com https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com https://www.googletagmanager.com; worker-src 'self' blob:; manifest-src 'self'; frame-src 'none'; upgrade-insecure-requests" />
46
<meta charset="UTF-8" />
57
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
68
<title>Provably Fair Crash Casinos: Verify Any Round Isn't Rigged | Tapiwa Makandigona</title>

blog/send-files-without-internet/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4+
<!-- Content-Security-Policy (see build_site.py) -->
5+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; script-src 'self' https://www.googletagmanager.com 'sha256-vaumWhGra9YRSnlwgR4nINjGokisWOWhk6zXCqHcnME='; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: https://play.google.com https://www.google-analytics.com https://www.googletagmanager.com; media-src 'self'; connect-src 'self' https://api.github.com https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com https://www.googletagmanager.com; worker-src 'self' blob:; manifest-src 'self'; frame-src 'none'; upgrade-insecure-requests" />
46
<meta charset="UTF-8" />
57
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
68
<title>Sending files between phone and PC with no internet | Tapiwa Makandigona</title>

blog/zero-dependency-react-component-library/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4+
<!-- Content-Security-Policy (see build_site.py) -->
5+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; script-src 'self' https://www.googletagmanager.com 'sha256-XtR//L26nkbWEhAnnwQosTw+tKBYXdW6uJ2jgDPpSRg='; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: https://play.google.com https://www.google-analytics.com https://www.googletagmanager.com; media-src 'self'; connect-src 'self' https://api.github.com https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com https://www.googletagmanager.com; worker-src 'self' blob:; manifest-src 'self'; frame-src 'none'; upgrade-insecure-requests" />
46
<meta charset="UTF-8" />
57
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
68
<title>Building a zero-dependency React component library in TypeScript | Tapiwa Makandigona</title>

contact/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<!doctype html>
22
<html lang="en">
33
<head>
4+
<!-- Content-Security-Policy (see build_site.py) -->
5+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: https://play.google.com https://www.google-analytics.com https://www.googletagmanager.com; media-src 'self'; connect-src 'self' https://api.github.com https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com https://www.googletagmanager.com; worker-src 'self' blob:; manifest-src 'self'; frame-src 'none'; upgrade-insecure-requests" />
46
<meta charset="utf-8" />
57
<title>Redirecting…</title>
68
<meta http-equiv="refresh" content="0; url=/" />

0 commit comments

Comments
 (0)