🔒 Security & Governance Audit¶
Date: 18 April 2026
Duration: ~3 hours (6:05 AM – 9:33 AM NZST)
Model: Claude Opus 4.6 (1M context)
Approach: 10 parallel audit agents + manual deep-dive + rubber-duck validation
Scope: 53 JS files (~1.5MB), 3 API functions, 8 repos, 50+ Hugo templates, MkDocs learning portal
Why This Was Done¶
AI-coded products are increasingly targeted for vulnerabilities. As the site grew to 46 tools with complex JS, 3 API endpoints, 6 pipeline repos, and a private learning portal — a comprehensive security audit was overdue. This session audited every nook and corner of the ecosystem.
Audit Methodology¶
Pass 1 — 6 Parallel Agents (different specialisations)¶
| Agent | Focus | Files Checked |
|---|---|---|
| JS XSS Audit | innerHTML, eval, URL params, localStorage, race conditions | 53 JS files |
| API & Secrets | CORS, auth, rate limiting, leaked credentials, git history | 3 API functions + repo root |
| Pipeline Security | GitHub Actions SHA pinning, PAT exposure, permissions scope | 10+ workflow files across 6 repos |
| Headers & Deps | CSP, HSTS, SRI hashes, CDN pinning, CORS config | staticwebapp.config.json + all templates |
| Data & Privacy | TOML injection, privacy claims accuracy, form security | All templates + JS |
| Learning Portal | MkDocs config, content exposure, internal data classification | Full learning-docs repo |
Pass 2 — 5 More Agents (different perspectives)¶
| Agent | Perspective |
|---|---|
| Defensive Review | "How would someone abuse these APIs?" |
| Data Flow Tracing | Trace every external input → DOM sink |
| Verify Fixes | Are pass-1 fixes actually correct? Regressions? |
| Privacy Compliance | GDPR/NZ Privacy Act, cookies, consent, data inventory |
| Rubber Duck | Independent validation of all findings + priority ranking |
Findings Summary¶
| Severity | Found | Fixed |
|---|---|---|
| 🔴 Critical | 4 | 4 ✅ |
| 🟠 High | 13 | 13 ✅ |
| 🟡 Medium | 13 | 13 ✅ |
| 🟢 Low | 1 | 1 ✅ |
| Total | 31 | 31 ✅ |
Plus: 5 repos made private, customer research docs moved to local-only.
Critical Findings & Fixes¶
C1: /api/stats had CORS * — anyone could scrape analytics¶
Before: Access-Control-Allow-Origin: * — any website could call the stats API and read GA4 page views, YouTube subscribers, GSC search queries, bio link clicks.
Fix: Restricted to https://www.aguidetocloud.com. Added rate limiting (30 req/min per IP). Sanitised all 7 error message paths to return generic errors only.
File: functions/api/stats.js
C2: Feedback API exposed user emails in public GitHub Discussions¶
Before: When users submitted feedback with an email, it was embedded directly in the public GitHub Discussion body: **Email:** user@example.com.
Fix: Emails are never published. The Discussion body now shows *Reply contact provided via form* instead. Server-side max length limits added (subject 200, message 5000, name 100, email 254).
File: functions/api/feedback.js
C3: Learning portal .env committed to git¶
Before: learning-docs/.env containing Azure tenant ID and subscription ID was committed in the initial commit and not in .gitignore.
Fix: Added .env to .gitignore, removed from git tracking with git rm --cached.
C4: Customer research docs in learning portal repo¶
Before: docs/customer-research/ contained Microsoft-confidential MBIE and Corrections NZ customer preparation documents.
Fix: Moved to local-only folder (C:\ssClawy\customer-research-private), removed from git and mkdocs.yml nav.
High-Priority Findings & Fixes¶
XSS Vulnerabilities (3 files)¶
deptime.js — Deprecation Timeline item IDs and urgency classes were injected raw into onclick handlers and CSS class names. A poisoned data file could break out of attributes and inject script.
Fix: Added safeId() (strips non-alphanumeric), safeClass() (whitelist of valid urgency values), and URL validation (/^https?:\/\// check before inserting into href). Applied to cards, timeline, AND modal (pass 2 caught the modal was missed in pass 1).
site-analytics.js — YouTube API response top.title was inserted into innerHTML without sanitisation. A malicious video title could execute XSS.
Fix: Wrapped in esc(). Grade field whitelisted to valid values only.
pomodoro.js — Object.assign(S.settings, JSON.parse(r)) merged localStorage JSON directly into settings, allowing prototype pollution via __proto__ keys.
Fix: Whitelist-only merge — only copies keys that exist in DEFAULTS.
Supply Chain Hardening¶
- SRI hashes added to all 6 CDN scripts (Chart.js, Mermaid, qr-code-styling, JSZip, jsQR, html2canvas)
- GitHub Actions SHA-pinned across all 6 repos (16 action references):
actions/checkout@11bd7190,actions/setup-python@0b93645e,actions/setup-node@49933ea5,peaceiris/actions-hugo@75d2e847,actions/github-script@60a0d830,azure/login@a65d910e - PAT removed from git clone URLs in all 5 pipeline repos — now uses
http.extraheaderpattern - Learning portal mkdocs-material pinned to version 9.6.12 (was unpinned)
CSP Tightening¶
| Change | Before | After |
|---|---|---|
script-src |
unsafe-inline + unsafe-eval |
unsafe-inline only (eval removed) |
img-src |
https: (any HTTPS) |
Specific domains only |
frame-ancestors |
Not set | 'self' |
Permissions-Policy |
camera, mic, geo only | + interest-cohort, payment, usb, bluetooth |
style-src |
'self' 'unsafe-inline' |
+ https://fonts.googleapis.com |
font-src |
'self' |
+ https://fonts.gstatic.com |
Feedback API Origin Check¶
Before: origin.includes('aguidetocloud.com') — bypassable with evil-aguidetocloud.com.
After: Exact URL match against allowed origins array. Empty/null origin rejected.
Template Parameter Injection¶
Two category templates injected Hugo front matter params into JS strings without escaping:
Fix: Changed to {{ .Params.category_filter | jsonify }} (proper JSON encoding).
Privacy & Compliance¶
Privacy Policy Page¶
Created /legal/ with comprehensive policy covering:
- What's collected (GA4, Clarity, feedback, localStorage, HIBP)
- Cookie inventory with types (analytics/functional/necessary)
- Third-party service table with privacy policy links
- NZ Privacy Act 2020 rights
- How to opt out (ad blocker, browser settings)
- Data retention periods
- Children's data statement
Footer Disclosure¶
Every page footer now includes: "This site uses anonymous analytics (Google Analytics & Microsoft Clarity) to improve content. No personal data is collected. Privacy Policy"
GA4 IP Anonymisation¶
Added anonymize_ip: true to GA4 config for additional privacy protection.
Repository Visibility¶
All pipeline repos made private to hide implementation details:
| Repo | Before | After |
|---|---|---|
| -ainews | 🌐 Public | 🔒 Private |
| m365-roadmap | 🌐 Public | 🔒 Private |
| service-health | 🌐 Public | 🔒 Private |
| cert-tracker | 🌐 Public | 🔒 Private |
| m365-deprecation-timeline | 🌐 Public | 🔒 Private |
| learning-docs | 🔒 Private | 🔒 Private (already) |
| aguidetocloud-revamp | 🌐 Public | 🌐 Public (intentional) |
What Was Verified CORRECT (Pass 2)¶
The verify-fixes agent confirmed:
- ✅ No
eval(),new Function(),document.write()in any JS file - ✅
esc()XSS helper consistently used across most tools - ✅ No secrets in git history (credentials.json never committed)
- ✅ Service worker is conservative (skips /api/, /data/, HTML)
- ✅ All CDN scripts version-pinned to exact versions
- ✅
crossorigin="anonymous"on CDN scripts - ✅ localStorage writes wrapped in try/catch in most tools
- ✅ YouTube video ID validation with regex before embedding
- ✅ No URL params reach DOM sinks without sanitisation
- ✅ No open redirects found
- ✅ No
postMessagewithout origin checks
Remaining Manual Action¶
| Item | Action | Priority |
|---|---|---|
Cloudflare Access for /cc/ |
CF Zero Trust → Access → Add self-hosted app → domain www.aguidetocloud.com, path /cc/ → Allow your email only |
🟠 High |
The current client-side password gate (SHA-256 hash comparison) can be bypassed via DevTools (sessionStorage.setItem('cc-ok', '1')). Cloudflare Access provides real server-side authentication for free.
Lessons Learned¶
esc()is great but not enough — inlineonclickhandlers needsafeId()too, and CSS classes need whitelisting. DOM construction is safer than string interpolation.- CORS
*on analytics APIs is the #1 risk — even though "it's just analytics", competitors can monitor your traffic, top content, and growth patterns. - In-memory rate limiters on CF Workers are weak — Map() resets on cold starts and doesn't share across isolates. For real protection, use Cloudflare WAF rate limiting rules.
- Hugo reserves "privacy" as a namespace — you cannot create
content/privacy.mdbecause Hugo's[privacy]config section conflicts. Use a different slug (e.g.,legal.md). - Email in public GitHub Discussions = PII leak — even with a "submissions are public" warning, users don't read it. Never include email in public content by default.
- Pipeline repos should be private — they contain API prompts, dedup logic, feed URLs, and authentication patterns. Zero downside to making them private.
origin.includes()is not security — substring matching for origin validation is trivially bypassable. Use exact URL comparison.- Second-pass audits catch what first-pass misses — the deptime.js modal XSS was only caught in pass 2 because pass 1 fixed cards/timeline but didn't check the modal rendering path.
Commits¶
| # | Message | Files | Repo |
|---|---|---|---|
| 1 | Security audit: fix CORS, XSS, CSP, SRI, origin checks | 16 | aguidetocloud-revamp |
| 2 | Security: update HIBP privacy disclosure | 1 | aguidetocloud-revamp |
| 3 | Security: pin all GitHub Actions to commit SHA | 5 | aguidetocloud-revamp |
| 4 | Security pass 2: PII protection, modal XSS, error sanitization | 3 | aguidetocloud-revamp |
| 5 | Security: add .env to .gitignore, remove from tracking | 2 | learning-docs |
| 6 | Security: remove customer research docs, pin deps, SHA-pin Actions | 5 | learning-docs |
| 7 | Security: pin actions to SHA, remove PAT from URLs, scope permissions | 1 each | 5 pipeline repos |
| 8 | Add privacy policy page + footer analytics disclosure | 4 | aguidetocloud-revamp |
Maintenance Checklist¶
After this audit, the following should be checked periodically:
- [ ] Monthly: Run
smoke-test.ps1— covers core pages + assets - [ ] Quarterly: Re-audit innerHTML usage in any NEW JS files
- [ ] On new tool build: Follow post-build security audit checklist (18 items in instructions)
- [ ] On CDN version bump: Regenerate SRI hashes
- [ ] On Action version bump: Update SHA pin + version comment
- [ ] On PAT rotation: Update
PERSONAL_PATin all 6 repos