Security Audit Report — z0rs.github.io
- Security
- SSRF
- CSRF
- HTTP Headers
- Gatsby
Executive Summary
During a comprehensive code audit of this blog's codebase, 7 distinct security findings were identified across multiple files. Findings range from critical SSRF to informational hygiene issues. The primary SSRF vulnerability has been fixed; the remaining findings represent acceptable risk or documentation concerns.
| # | Finding | File | Severity | Status |
|---|---|---|---|---|
| 1 | SSRF via MDX frontmatter URLs | gatsby-node.js | Critical | Fixed |
| 2 | .env.example documents real secret names | .env.example | Low | Unfixed |
| 3 | GA Measurement ID embedded client-side | root-element.js | Low | By design |
| 4 | Hardcoded site URL in WebmentionAside | templates/article.js, ctf.js | Low | Unfixed |
| 5 | dangerouslySetInnerHTML in SSR | gatsby-ssr.js | Low | Acceptable |
| 6 | Missing HTTP security headers | gatsby-ssr.js | Medium | Unfixed |
| 7 | CSRF / rate limiting | N/A | N/A | Not applicable |
Finding 1 — SSRF via MDX Frontmatter URLs
Severity: Critical
Status: Fixed
File: gatsby-node.js lines 6–18, 88–186
Description
Three frontmatter fields — featuredImage, embeddedImages, and logo — accept arbitrary URLs that are passed
directly to createRemoteFileNode() during Gatsby's build phase. Without validation, an attacker who can push MDX
content (or modify existing files) can trigger HTTP requests to internal infrastructure from the build server's network.
Attack Scenarios
Internal network scanning:
featuredImage: "http://169.254.169.254/latest/meta-data/" # AWS metadatafeaturedImage: "http://192.168.1.1/admin" # Router adminfeaturedImage: "http://10.0.0.1:5432" # Database port scan
Build servers often run on cloud infrastructure (AWS EC2, GCP, Azure) with access to internal VPCs and metadata
endpoints. The 169.254.169.254 endpoint provides instance credentials and IAM roles if the build server has a service
account.
Local service access:
featuredImage: "http://localhost:8080/debug"featuredImage: "http://127.0.0.1:9000/health"
Fix Applied
function isAllowedUrl(url) { try { const { hostname, protocol } = new URL(url); if (!['http:', 'https:'].includes(protocol)) return false; const blocked = ['localhost', '127.0.0.1', '0.0.0.0']; if (blocked.includes(hostname)) return false; const isPrivate = /^10\.|^172\.(1[6-9]|2\d|3[01])\.|^192\.168\.|^127\./.test(hostname) || hostname === '169.254.169.254'; // AWS metadata return !isPrivate; } catch { return false; }}
Each createRemoteFileNode() call is now gated with isAllowedUrl(). Blocked URLs are logged with
[gatsby-node] Blocked SSRF attempt and skipped without fetching.
Finding 2 — .env.example Documents Real Secret Names
Severity: Low
Status: Unfixed
File: .env.example
Description
.env.example contains placeholder entries that reveal the names of real credentials the project once used or planned
to use:
FAUNA_KEY=""
GOOGLE_PROJECT_ID=""
GOOGLE_CLIENT_EMAIL=""
GOOGLE_PRIVATE_KEY=""
GOOGLE_GA4_PROPERTY_ID=""
GATSBY_API_URL=""
CK_FORM_ID=""
CK_API_KEY=""
The actual .env file was never committed. However, the example file itself is a reconnaissance aid — FAUNA_KEY,
CK_API_KEY, and GOOGLE_PRIVATE_KEY specifically telegraph what credentials an attacker should look for if they
compromise the environment.
Proof of Concept
- Clone the repository
- Read
.env.example - Infer that the project uses FaunaDB, Google Cloud service accounts, and Classic Forms
- Attempt to locate these credentials via supply chain attacks, leaked CI logs, or public code commits
Remediation
Remove credential-type variable names from .env.example. Use generic names:
# Before (reveals credential types)
FAUNA_KEY=""
# After (generic)
DB_SECRET=""
Or remove .env.example entirely if the project no longer uses any of these services.
Finding 3 — Google Analytics Measurement ID Client-Side
Severity: Low
Status: By design
File: src/components/root-element.js, gatsby-config.mjs
Description
process.env.GATSBY_GA_MEASUREMENT_ID is baked into the client bundle at build time via two <Script> tags in
root-element.js:
<Script src={`https://www.googletagmanager.com/gtag/js?id=${process.env.GATSBY_GA_MEASUREMENT_ID}`} ... />gtag('config', process.env.GATSBY_GA_MEASUREMENT_ID, { send_page_view: false })
This is expected behavior in Gatsby — all GATSBY_* env vars are public by design. The GA Measurement ID is a public
token intended for client-side inclusion.
The only risk is if the token is exposed in server-side logs or error messages. The partytownProxiedURLs in
gatsby-config.mjs references the same variable — if unset, Partytown proxies a malformed URL
https://www.googletagmanager.com/gtag/js?id= which is cosmetic, not exploitable.
Finding 4 — Hardcoded Site URL in WebmentionAside
Severity: Low
Status: Unfixed
Files: src/templates/article.js, src/templates/ctf.js
Description
Both templates hardcode the site URL for WebmentionAside:
<WebmentionAside target={`https://z0rs.github.io${slug}`} />
siteMetadata.siteUrl is available in the template context but is not used. If the site is ever deployed to a different
domain (e.g., a custom domain), Webmentions would point at the wrong domain silently.
Proof of Concept
- Change
siteUrlingatsby-config.mjstohttps://example.com - Build — WebmentionAside still sends webmentions to
z0rs.github.io - No webmentions received at the new domain
Fix
<WebmentionAside target={`${siteUrl}${slug}`} />
Finding 5 — dangerouslySetInnerHTML in SSR
Severity: Low
Status: Acceptable
File: gatsby-ssr.js lines 59–86
Description
onRenderBody uses dangerouslySetInnerHTML to inject @font-face declarations:
dangerouslySetInnerHTML={{ __html: ` @font-face { font-family: 'Inconsolata'; font-weight: 400; src: url(/fonts/Inconsolata-Regular.woff2) format('woff2'); } ... `}}
The content is entirely static — hardcoded font declarations with only local /fonts/ paths. No user-controlled input,
no variable interpolation, no MDX rendering. This is React's recommended pattern for injecting global CSS.
Not exploitable.
Finding 6 — Missing HTTP Security Headers
Severity: Medium
Status: Unfixed
File: gatsby-ssr.js
Description
onRenderBody sets favicon links and font preloads but does not set HTTP security headers. The following are absent:
| Header | Purpose |
|---|---|
Content-Security-Policy | Prevents XSS by restricting resource loading |
X-Frame-Options | Clickjacking protection |
X-Content-Type-Options: nosniff | Prevents MIME sniffing |
Referrer-Policy | Controls referrer header leakage |
Permissions-Policy | Disables browser features not used |
Gatsby's built-in server (gatsby serve) does not set security headers by default. If the site is served directly
without a CDN, all headers are absent.
Fix
Add to onRenderBody in gatsby-ssr.js:
export const onRenderBody = ({ setHeadComponents }) => { setHeadComponents([ // existing components... <meta key="x-frame-options" httpEquiv="X-Frame-Options" content="DENY" />, <meta key="x-content-type-options" httpEquiv="X-Content-Type-Options" content="nosniff" />, <meta key="referrer-policy" httpEquiv="Referrer-Policy" content="strict-origin-when-cross-origin" /> ]);};
Content-Security-Policy requires careful crafting based on the site's actual resource loading patterns and is beyond a
simple audit fix.
Finding 7 — CSRF / Rate Limiting
Severity: N/A
Status: Not applicable
This is a static site generated entirely at build time. There are no src/api/ routes, no server-side API
endpoints, and no user-submitted form data processed by the application.
CSRF and rate limiting are relevant for dynamic server-side applications that process state-changing requests. Static generation produces no runtime request handling — every page is a pre-rendered HTML file served from CDN or GitHub Pages.
The reaction system (which would have required CSRF and rate limiting) was removed because GitHub Pages cannot host server-side functions.
Disclosure Timeline
- 2026-04-29: Audit started — all files reviewed including gatsby-node.js, components, templates, configs, MDX content, and package.json
- 2026-04-29: SSRF vulnerability identified and fixed in
gatsby-node.js - 2026-04-29: Report published with all 7 findings documented