Skip to content

Security Audit — OWASP Top 10 Review (2026-04-08) #20

@dinooo13

Description

@dinooo13

Date: 2026-04-08
Scope: Full codebase — application code, CI/CD pipelines, content schemas, dependency configuration
Standard: OWASP Top 10 (2021), general web security best practices
Site type: Nuxt 4 static site (SSG), deployed via FTP to a shared host

Full findings are in security-audit.md on branch claude/security-audit-owasp-isrCR.


Executive Summary

The site has a small attack surface owing to its static nature — no server-side runtime, no user accounts, no forms, and no database at runtime. The application code itself is clean: no v-html usage, no raw innerHTML, no eval, and content comes exclusively from YAML files committed to the repository.

However, pnpm audit reveals 21 known CVEs in the dependency tree (10 high, 11 moderate), including SSRF and XSS vulnerabilities in nuxt-og-image that affect the static generation process.

The significant findings are concentrated in four areas:

  1. Known CVEs in dependencies — 21 vulnerabilities, several directly relevant to this site.
  2. Missing HTTP security headers — the single highest-impact gap for a production site.
  3. Insecure FTP deployment — credentials and file content transmitted in plaintext.
  4. GitHub Actions supply chain risk — third-party actions pinned to mutable version tags rather than immutable commit SHAs.

Findings


FIND-00 · Known CVEs in Dependency Tree (21 vulnerabilities: 10 high, 11 moderate)

Severity: High
OWASP: A06:2021 – Vulnerable and Outdated Components

pnpm audit surfaces 21 known vulnerabilities. Most are in build-time tooling (Vite, ESLint devtools) and don't affect the generated static output. However, several affect packages active during pnpm generate or within nuxt-og-image's own server component.

High severity (10):

Package Affected versions Advisory Description
nuxt-og-image <6.2.5 GHSA-pqhr-mp3f-hrpp SSRF — user-controlled parameters can trigger server-side requests
node-forge <=1.3.3 GHSA-2328-f5f3-gj25 basicConstraints bypass in certificate chain verification
node-forge <1.4.0 GHSA-q67f-28xg-22rw Ed25519 signature forgery
node-forge <1.4.0 GHSA-5m6q-g25r-mvwx DoS via infinite loop
node-forge <1.4.0 GHSA-ppp5-5v6c-4jwp RSA-PKCS signature forgery
lodash >=4.0.0 <=4.17.23 GHSA-r5fr-rjxr-66jc Code injection via _.template
defu <=6.1.4 GHSA-737v-mqg7-c878 Prototype pollution via __proto__ key
picomatch <2.3.2, >=4.0.0 <4.0.4 GHSA-c2c7-rcm5-vvqj ReDoS via extglob
vite >=7.1.0 <=7.3.1 GHSA-v2wj-q39q-566r server.fs.deny bypass
vite >=7.0.0 <=7.3.1 GHSA-p9ff-h696-f583 Arbitrary file read via dev server

Moderate severity (11):

Package Advisory Description
nuxt-og-image GHSA-mg36-wvcr-m75h Reflected XSS via query parameter injection
nuxt-og-image GHSA-c7xp-q6q8-hg76 DoS via unbounded image dimensions
yaml GHSA-48c2-rrv3-qjmp Stack overflow via deeply nested collections
serialize-javascript GHSA-qj8w-gfj5-8c6v CPU exhaustion DoS
lodash GHSA-f23m-r3pf-42rh Prototype pollution via array functions
picomatch GHSA-3v7f-55p6-f55p Method injection in POSIX character classes
brace-expansion GHSA-f886-m6hf-6m8v Zero-step sequence causes process hang (×2 paths)
vite GHSA-4w7w-66w2-5vf9 Path traversal in optimized deps .map handling

Risk context:

  • nuxt-og-image XSS/SSRF: Active during pnpm generate. Lower risk in pure SSG — but would be critical if dynamic OG generation is ever enabled.
  • vite arbitrary file read (GHSA-p9ff-h696-f583): Affects the dev server — critical if pnpm dev is ever exposed to untrusted network access.
  • node-forge issues: Active via @nuxt/image > ipx > listhen during dev server / generation.

Remediation:

pnpm update nuxt-og-image --latest   # resolves 3 advisories
pnpm audit --fix
pnpm audit                            # verify

FIND-01 · Missing HTTP Security Headers

Severity: High
OWASP: A05:2021 – Security Misconfiguration

No security-relevant HTTP response headers are configured anywhere — not in nuxt.config.ts, not in a public/.htaccess, and not in any server configuration file.

Header Risk if absent
Content-Security-Policy XSS via injected scripts
X-Frame-Options: DENY Clickjacking — site can be framed
X-Content-Type-Options: nosniff MIME-type sniffing attacks
Referrer-Policy Full URL leaks in Referer header
Permissions-Policy Unwanted access to camera, mic, geolocation
Strict-Transport-Security (HSTS) Downgrade attacks

Recommended public/.htaccess (Apache):

Header always set X-Frame-Options "DENY"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';"

FIND-02 · Insecure FTP Deployment (Plaintext Credentials + Data in Transit)

Severity: High
OWASP: A02:2021 – Cryptographic Failures
File: .github/workflows/deploy.yml:44-49

The deployment workflow uses SamKirkland/FTP-Deploy-Action without specifying a secure protocol, defaulting to plain FTP — transmitting credentials and file content in cleartext.

Remediation: Add protocol: ftps or migrate to rsync over SSH with a deploy key.

with:
  server: ${{ secrets.HOST }}
  username: ${{ secrets.USERNAME }}
  password: ${{ secrets.PASSWORD }}
  protocol: ftps          # add this
  local-dir: ./.output/public/
  server-dir: ./httpdocs/

FIND-03 · GitHub Actions — Not Pinned to Commit SHAs

Severity: High
OWASP: A08:2021 – Software and Data Integrity Failures
Files: .github/workflows/ci.yml, deploy.yml, claude.yml

All action references use mutable version tags. If any upstream repo is compromised, malicious code runs with access to all repository secrets (ANTHROPIC_API_KEY, HOST, USERNAME, PASSWORD).

uses: actions/checkout@v6                        # mutable
uses: SamKirkland/FTP-Deploy-Action@v4.3.6       # mutable
uses: anthropics/claude-code-action@v1           # mutable

Remediation: Pin to commit SHAs. Use Dependabot or step-security/harden-runner to automate.


FIND-04 · Claude Code Action — Overly Broad Permissions on Untrusted Triggers

Severity: Medium
OWASP: A01:2021 – Broken Access Control
File: .github/workflows/claude.yml

The workflow is triggered by issue/PR comments (untrusted content) with:

permissions:
  contents: write
  pull-requests: write
  issues: write
  id-token: write    # can request OIDC tokens → authenticate to cloud providers
  actions: read

A prompt-injection attack in an issue comment could manipulate the Claude agent into unintended write operations. id-token: write is especially broad.

Remediation: Remove id-token: write if OIDC cloud auth is unused. Restrict triggers to repo collaborators.


FIND-05 · Missing URL Validation on Lab Content Fields

Severity: Medium
OWASP: A03:2021 – Injection
File: content.config.ts:94-95

url: z.string().optional(),      // ← no .url() validation
repoUrl: z.string().optional(),  // ← no .url() validation

These values flow directly into <UButton :to="lab.url" target="_blank" />. A javascript: URI in a YAML file would execute JS on click. The talks schema correctly uses z.string().url().

Remediation:

url: z.string().url().optional(),
repoUrl: z.string().url().optional(),

FIND-06 · CI Missing --frozen-lockfile

Severity: Medium
OWASP: A08:2021 – Software and Data Integrity Failures
File: .github/workflows/ci.yml:29

CI uses pnpm install (no lockfile enforcement) while deploy uses pnpm install --frozen-lockfile. CI may test a different dependency set than what gets deployed.

Remediation: run: pnpm install --frozen-lockfile


FIND-07 · rel="noopener noreferrer" Not Explicit on target="_blank" Links

Severity: Low
Files: app/pages/labs/[slug].vue:64,72, app/pages/speaking/[slug].vue:91,220, app/components/LabCard.vue:69,78

Nuxt UI handles this automatically for external links, but the protection is implicit. Should be explicit.


FIND-08 · devtools: { enabled: true } Committed to Source

Severity: Low / Informational
File: nuxt.config.ts:12-14

Devtools are dev-only but the explicit enabled: true creates unnecessary noise and a minor risk if production preview is run locally.

Remediation: Remove the block (default behaviour is already dev-only enabled).


FIND-09 · better-sqlite3 in dependencies Instead of devDependencies

Severity: Low / Informational
File: package.json:21

Build-time-only package listed as a runtime dependency. Unnecessarily expands the production dependency surface.


FIND-10 · Transitive Dependency Overrides Undocumented

Severity: Informational
File: package.json:33-37

h3, socket.io-parser, flatted are pinned via pnpm.overrides without explanation. These mitigations require active maintenance and should be documented with the CVE rationale.


FIND-11 · actions/checkout Version Inconsistency

Severity: Informational

ci.yml and deploy.yml use @v6, claude.yml uses @v4. Standardise (and pin to SHAs per FIND-03).


FIND-12 · robots.txt Missing Sitemap Directive

Severity: Informational
File: public/robots.txt

Minor: no Sitemap: directive. Not a security issue.


Positive Findings

  • No v-html usage anywhere — all content rendered via Vue's auto-escaping {{ }}
  • No eval(), innerHTML, or Function() constructor anywhere
  • Content from committed YAML only — no runtime injection surface
  • Zod schema validation on all content collections
  • Secrets correctly stored in GitHub Secrets, never hardcoded
  • --frozen-lockfile correctly used in the deploy workflow
  • "private": true in package.json

Remediation Priority

Priority Finding Effort
P1 FIND-00 — 21 known CVEs Low (pnpm update nuxt-og-image --latest && pnpm audit --fix)
P1 FIND-01 — Missing HTTP security headers Low (.htaccess change)
P1 FIND-02 — FTP plaintext transport Low (add protocol: ftps)
P1 FIND-03 — Actions not pinned to commit SHAs Medium
P2 FIND-04 — Claude Action broad permissions Low
P2 FIND-05 — Missing URL validation on lab fields Low
P2 FIND-06 — CI missing --frozen-lockfile Trivial
P3 FIND-07 — rel not explicit on _blank links Low
P3 FIND-08 — devtools: enabled: true in source Trivial
P3 FIND-09 — better-sqlite3 in wrong dep category Trivial
P4 FIND-10 — Transitive dep overrides undocumented Low
P4 FIND-11 — checkout version inconsistency Trivial
P4 FIND-12 — robots.txt missing sitemap Trivial

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions