Skip to content

Promote sandbox to main (2026-07-08)#36

Merged
TOM-BOHN merged 1 commit into
mainfrom
sandbox
Jul 8, 2026
Merged

Promote sandbox to main (2026-07-08)#36
TOM-BOHN merged 1 commit into
mainfrom
sandbox

Conversation

@github-actions

@github-actions github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Sandbox validated and ready for production

This PR was opened automatically after sandbox was validated by
this workflow run.

  • Build (npm run build): passed
  • Commit validated: 4a712c827c1487c0290ffbd47d2973b771415e6a

Commits included in this promotion

Review checklist

  • Build passed in CI (see above)
  • Changes reviewed and look correct for production
  • Ready to deploy (merging this PR triggers the GitHub Pages deploy workflow)

This PR requires manual human review and must be merged by a person.
This automation will never merge it.

…dings (#35)

* fix(deps): resolve 12 Dependabot vulnerabilities via npm audit + postcss override

Runs npm audit fix to resolve 11 of 12 flagged vulnerabilities in
transitive devDependencies (eslint tooling, puppeteer chain, gray-matter):

- @babel/core: arbitrary file read via sourceMappingURL (GHSA-4x5r-pxfx-6jf8)
- ajv: ReDoS via $data option (GHSA-2g4f-4pwh-qvx6)
- basic-ftp: path traversal + CRLF injection + 2x DoS (critical) (GHSA-5rq4-664w-9x2c, GHSA-6v7q-wjvx-w8wg, GHSA-rp42-5vxx-qpwr, GHSA-rpmf-866q-6p89)
- brace-expansion: zero-step sequence DoS (GHSA-f886-m6hf-6m8v)
- flatted: unbounded recursion DoS + prototype pollution (GHSA-25h7-pfq9-p65f, GHSA-rf6f-7fwh-wjgh)
- ip-address: XSS in Address6 HTML-emitting methods (GHSA-v2v4-37r5-5v8g)
- js-yaml: quadratic-complexity DoS via merge key handling (GHSA-h67p-54hq-rp68)
- minimatch: 3x ReDoS via wildcard/extglob backtracking (GHSA-3ppc-4f35-3m26, GHSA-7r86-cg39-jmmj, GHSA-23c5-xmqv-rm74)
- picomatch: method injection + ReDoS (GHSA-3v7f-55p6-f55p, GHSA-c2c7-rcm5-vvqj)
- ws: uninitialized memory disclosure + memory exhaustion DoS (GHSA-58qx-3vcg-4xpx, GHSA-96hv-2xvq-fx4p)

The 12th finding (postcss XSS via unescaped </style>, GHSA-qx2v-qp2m-jg93)
could not be fixed by npm audit fix because Next.js bundles its own
internal copy of postcss (node_modules/next/node_modules/postcss@8.4.31)
that npm's suggested resolution would only fix by downgrading next to
9.3.3 - not viable. Instead:

- Bump the direct postcss devDependency to ^8.5.10 (patched).
- Add a package.json "overrides" entry pinning postcss to ^8.5.10 for
  every nested copy in the dependency tree, including the one bundled
  inside next, without downgrading next itself.

Result: `npm audit` now reports 0 vulnerabilities (down from 12: 1 low,
6 moderate, 4 high, 1 critical). Verified `npm run build` and
`npx tsc --noEmit` still succeed with no regressions.

Co-authored-by: Thomas Bohn <bohn.tl@gmail.com>

* fix(security): sanitize blog HTML, validate external podcast links, harden path resolution

Addresses code-scanning findings in JS/TS application code:

1. XSS via dangerouslySetInnerHTML with unsanitized Markdown HTML
   (lib/blog.ts, app/blog/[slug]/page.tsx). remark-html does not sanitize
   its output, so raw HTML embedded in a blog Markdown file (local or
   Medium-synced) would be rendered as-is in the browser. Added
   sanitize-html with an explicit tag/attribute/scheme allowlist so
   <script>, event handlers, iframes, and non-http(s) URLs are stripped
   before content ever reaches dangerouslySetInnerHTML, while preserving
   normal formatting (headings, lists, links, code, images).

2. Unvalidated external URL used as link href
   (components/xanga/applets/NowListeningApplet.tsx). Podcast episode
   links are sourced from third-party RSS/iTunes responses (including an
   external RSS proxy), then rendered directly into an <a href>. A
   compromised feed or proxy could inject a javascript:/data: URL. Added
   toSafeHttpUrl() to allowlist only http/https schemes before a link is
   rendered, falling back to '#' otherwise.

3. Defense-in-depth path resolution guard (lib/blog.ts getBlogPost).
   Added resolvePostPath() to verify the resolved file path stays inside
   content/blog/ before reading it, guarding against path traversal if
   this function is ever reused outside the current build-time-only,
   generateStaticParams-derived call sites.

Verified with npx tsc --noEmit, npm run build, and a rendered diff of
generated blog HTML (headings/lists/links/code/strong all preserved) to
confirm no visual or content regressions.

Co-authored-by: Thomas Bohn <bohn.tl@gmail.com>

* fix(ci): move github.sha/steps.outcome interpolation out of run: blocks

CodeQL's Actions query suite flags any ${{ ... }} expression embedded
directly inside a run: script block as a potential script-injection sink,
since the substitution happens before the shell ever runs and can allow
control-character/quote injection when the underlying context value is
attacker-influenced.

The three flagged spots in promote-sandbox-to-main.yml only interpolated
github.sha (an immutable 40-char commit hash) and steps.lint.outcome (a
fixed GitHub-controlled enum), so this workflow was not exploitable via
untrusted PR/issue input. Fixed anyway for defense-in-depth and
consistency with the env:-based pattern already used elsewhere in this
same file (e.g. RUN_URL, VALIDATED_SHA in other steps):

- 'Record lint outcome' step: steps.lint.outcome -> $LINT_OUTCOME env var
- 'Close stale build-failure issue' step: github.sha -> $VALIDATED_SHA env var
- 'Close stale promotion-failure issue' step: github.sha -> $VALIDATED_SHA env var

No behavior change; only the mechanism for getting these values into the
shell script changed.

Co-authored-by: Thomas Bohn <bohn.tl@gmail.com>

* chore(ci): pin third-party GitHub Actions to commit SHAs

Supply-chain hardening flagged as a low-priority code-scanning finding:
all actions/* steps referenced floating major-version tags (@v4, @V3),
which can be repointed by the upstream maintainer (or an attacker who
compromises their account/repo) to a different commit without any
signal in this repo. Pinned every actions/* step in both workflows to
the exact commit SHA currently published under that tag, with the
resolved version kept as a trailing comment for readability/auditing:

- actions/checkout -> 34e114876b0b11c390a56381ad16ebd13914f8d5 (v4.3.1)
- actions/setup-node -> 49933ea5288caeca8642d1e84afbd3f7d6820020 (v4.4.0)
- actions/configure-pages -> 1f0c5cde4bc74cd7e1254d0cb4de8d49e9068c7d (v4.0.0)
- actions/upload-pages-artifact -> 56afc609e74202658d3ffba0e8f6dda462b719fa (v3.0.1)
- actions/deploy-pages -> d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e (v4.0.5)

No behavior change - these SHAs are exactly what the @v4/@V3 tags
currently resolve to.

Co-authored-by: Thomas Bohn <bohn.tl@gmail.com>

* fix(security): add CSP + Referrer-Policy meta tags, non-guessable JSONP callback name

Addresses the remaining medium/low-priority code-scanning findings:

1. Missing security headers. This site is statically exported to GitHub
   Pages with no server, so it cannot send custom HTTP response headers
   (no _headers file support, no headers() config). Added the strongest
   mitigation available in that environment: a Content-Security-Policy
   and Referrer-Policy via <meta> tags in the root layout, scoped to the
   exact external origins the app calls (itunes.apple.com for the
   Now Listening JSONP request, api.allorigins.win + api.open-meteo.com
   for fetch() calls, images.credly.com for certification badge images,
   w.soundcloud.com for the Vibe Tunes iframe embed). script-src requires
   'unsafe-inline' because Next's static export embeds a per-build RSC
   hydration payload as inline <script> content with no server available
   to issue nonces/hashes - documented inline. Note that X-Frame-Options,
   Strict-Transport-Security, and Permissions-Policy have no meta-tag
   equivalent and genuinely require a real HTTP header, which isn't
   achievable on GitHub Pages alone (would need a CDN/edge layer such as
   Cloudflare in front of the custom domain - out of scope for this repo).

2. Math.random() used to build a JSONP callback name in
   NowListeningApplet - switched to crypto.randomUUID() to avoid a
   predictable/collidable global name on window. Not security-critical
   (no auth/token use), fixed for defense-in-depth per the original
   audit's low-priority note.

Validated with a headless-browser pass across every page (including the
Xanga sidebar theme, which mounts every applet that calls an external
API or embeds an iframe) listening for 'securitypolicyviolation' events
and console errors: zero CSP violations, zero console errors. Confirmed
via curl that the one widget showing no data (Hit Counter) is failing
because countapi.xyz is unreachable at the network level (pre-existing,
unrelated to this change), not because of the new CSP.

Co-authored-by: Thomas Bohn <bohn.tl@gmail.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
@github-actions
github-actions Bot requested a review from TOM-BOHN July 8, 2026 02:32
@TOM-BOHN
TOM-BOHN merged commit 073fbf9 into main Jul 8, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant