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:
- Known CVEs in dependencies — 21 vulnerabilities, several directly relevant to this site.
- Missing HTTP security headers — the single highest-impact gap for a production site.
- Insecure FTP deployment — credentials and file content transmitted in plaintext.
- 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):
Moderate severity (11):
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 |
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.mdon branchclaude/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-htmlusage, no rawinnerHTML, noeval, and content comes exclusively from YAML files committed to the repository.However,
pnpm auditreveals 21 known CVEs in the dependency tree (10 high, 11 moderate), including SSRF and XSS vulnerabilities innuxt-og-imagethat affect the static generation process.The significant findings are concentrated in four areas:
Findings
FIND-00 · Known CVEs in Dependency Tree (21 vulnerabilities: 10 high, 11 moderate)
Severity: High
OWASP: A06:2021 – Vulnerable and Outdated Components
pnpm auditsurfaces 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 duringpnpm generateor withinnuxt-og-image's own server component.High severity (10):
nuxt-og-image<6.2.5node-forge<=1.3.3node-forge<1.4.0node-forge<1.4.0node-forge<1.4.0lodash>=4.0.0 <=4.17.23_.templatedefu<=6.1.4__proto__keypicomatch<2.3.2,>=4.0.0 <4.0.4vite>=7.1.0 <=7.3.1server.fs.denybypassvite>=7.0.0 <=7.3.1Moderate severity (11):
nuxt-og-imagenuxt-og-imageyamlserialize-javascriptlodashpicomatchbrace-expansionvite.maphandlingRisk context:
nuxt-og-imageXSS/SSRF: Active duringpnpm generate. Lower risk in pure SSG — but would be critical if dynamic OG generation is ever enabled.vitearbitrary file read (GHSA-p9ff-h696-f583): Affects the dev server — critical ifpnpm devis ever exposed to untrusted network access.node-forgeissues: Active via@nuxt/image > ipx > listhenduring dev server / generation.Remediation:
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 apublic/.htaccess, and not in any server configuration file.Content-Security-PolicyX-Frame-Options: DENYX-Content-Type-Options: nosniffReferrer-PolicyRefererheaderPermissions-PolicyStrict-Transport-Security(HSTS)Recommended
public/.htaccess(Apache):FIND-02 · Insecure FTP Deployment (Plaintext Credentials + Data in Transit)
Severity: High
OWASP: A02:2021 – Cryptographic Failures
File:
.github/workflows/deploy.yml:44-49The deployment workflow uses
SamKirkland/FTP-Deploy-Actionwithout specifying a secure protocol, defaulting to plain FTP — transmitting credentials and file content in cleartext.Remediation: Add
protocol: ftpsor migrate to rsync over SSH with a deploy key.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.ymlAll 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).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.ymlThe workflow is triggered by issue/PR comments (untrusted content) with:
A prompt-injection attack in an issue comment could manipulate the Claude agent into unintended write operations.
id-token: writeis especially broad.Remediation: Remove
id-token: writeif 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-95These values flow directly into
<UButton :to="lab.url" target="_blank" />. Ajavascript:URI in a YAML file would execute JS on click. Thetalksschema correctly usesz.string().url().Remediation:
FIND-06 · CI Missing
--frozen-lockfileSeverity: Medium
OWASP: A08:2021 – Software and Data Integrity Failures
File:
.github/workflows/ci.yml:29CI uses
pnpm install(no lockfile enforcement) while deploy usespnpm install --frozen-lockfile. CI may test a different dependency set than what gets deployed.Remediation:
run: pnpm install --frozen-lockfileFIND-07 ·
rel="noopener noreferrer"Not Explicit ontarget="_blank"LinksSeverity: Low
Files:
app/pages/labs/[slug].vue:64,72,app/pages/speaking/[slug].vue:91,220,app/components/LabCard.vue:69,78Nuxt UI handles this automatically for external links, but the protection is implicit. Should be explicit.
FIND-08 ·
devtools: { enabled: true }Committed to SourceSeverity: Low / Informational
File:
nuxt.config.ts:12-14Devtools are dev-only but the explicit
enabled: truecreates 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-sqlite3independenciesInstead ofdevDependenciesSeverity: Low / Informational
File:
package.json:21Build-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-37h3,socket.io-parser,flattedare pinned viapnpm.overrideswithout explanation. These mitigations require active maintenance and should be documented with the CVE rationale.FIND-11 ·
actions/checkoutVersion InconsistencySeverity: Informational
ci.ymlanddeploy.ymluse@v6,claude.ymluses@v4. Standardise (and pin to SHAs per FIND-03).FIND-12 ·
robots.txtMissing Sitemap DirectiveSeverity: Informational
File:
public/robots.txtMinor: no
Sitemap:directive. Not a security issue.Positive Findings
v-htmlusage anywhere — all content rendered via Vue's auto-escaping{{ }}eval(),innerHTML, orFunction()constructor anywhere--frozen-lockfilecorrectly used in the deploy workflow"private": trueinpackage.jsonRemediation Priority
pnpm update nuxt-og-image --latest && pnpm audit --fix).htaccesschange)protocol: ftps)--frozen-lockfilerelnot explicit on_blanklinksdevtools: enabled: truein sourcebetter-sqlite3in wrong dep categorycheckoutversion inconsistencyrobots.txtmissing sitemap