Skip to content

fix: close stored XSS, anon RPC, and rate limiting gaps in marketplace#32

Merged
siracusa5 merged 2 commits intomainfrom
security/audit-fixes
Mar 17, 2026
Merged

fix: close stored XSS, anon RPC, and rate limiting gaps in marketplace#32
siracusa5 merged 2 commits intomainfrom
security/audit-fixes

Conversation

@siracusa5
Copy link
Copy Markdown
Collaborator

Summary

Pre-launch security hardening for the marketplace backend. Closes three critical/high findings from the security audit: stored XSS via community plugin markdown rendering, unauthenticated RPC access to inflate install counts, and missing rate limiting across all API routes.

Changes

  • Stored XSS fix — replaced the raw dangerouslySetInnerHTML pipeline with sanitize-html (allowlist-based) in app/plugins/[slug]/page.tsx. Blocks event handlers, <script> tags, javascript: URLs, and all other non-allowlisted content while preserving Tailwind classes for styling.
  • Anon RPC lockdownmarketplace/supabase/migrations/00003_revoke_anon_rpc.sql revokes EXECUTE on increment_install_count from anon and authenticated roles. Migration already applied to production. The /api/install route continues to work via service_role.
  • Rate limiting — new marketplace/lib/rate-limit.ts: in-memory sliding-window limiter (Map-based, TTL cleanup). Applied to:
    • /api/install: 5 requests per slug per IP per hour
    • /api/search: 60 requests per IP per minute
    • /api/register: 10 requests per IP per hour
    • All return 429 with Retry-After header
  • .gitignore — added *.tsbuildinfo (Next.js incremental build artifact)

Test Plan

  • Local tests pass (68 core + 191 desktop = 259 tests)
  • pnpm build:marketplace passes with no type errors
  • XSS vectors verified stripped: <img onerror>, <script>, javascript: href, inline event handlers
  • Tailwind classes verified preserved on safe tags
  • Rate limiter: allows N requests, returns 429 with correct Retry-After on N+1
  • Supabase migration applied and confirmed via MCP
  • CI checks pass

Notes

Used sanitize-html instead of isomorphic-dompurify (specified in audit plan) — isomorphic-dompurify has a known jsdom bundling incompatibility with Next.js App Router where browser/default-stylesheet.css can't be resolved from the webpack output. sanitize-html is the idiomatic server-side choice and has no DOM dependency.

Rate limiter is in-process (single-instance safe for launch). When traffic grows, swap for Cloudflare rate limiting rules or Upstash — the interface in rate-limit.ts is the same either way.

Deferred (follow-up PR): search ilike wildcard sanitization, security headers middleware, Tauri CSP, .env.example files, board server CORS fix.

…tplace

- Replace raw innerHTML rendering with sanitize-html (allowlist-based) in
  plugin detail page — blocks stored XSS via community plugin markdown
- Add marketplace/lib/rate-limit.ts: in-memory sliding-window limiter
- Rate limit /api/install (5/slug/IP/hr), /api/search (60/IP/min),
  /api/register (10/IP/hr) with 429 + Retry-After responses
- Add migration 00003: REVOKE EXECUTE on increment_install_count from
  anon + authenticated roles (migration applied to production)
- Add *.tsbuildinfo to .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@siracusa5 siracusa5 changed the title security: close stored XSS, anon RPC, and rate limiting gaps in marketplace fix: close stored XSS, anon RPC, and rate limiting gaps in marketplace Mar 17, 2026
Rate limiting must run before authentication so that failed auth
attempts increment the counter — otherwise an attacker can brute-force
the API key without ever hitting the rate limit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@siracusa5 siracusa5 merged commit a117a06 into main Mar 17, 2026
5 checks passed
@siracusa5 siracusa5 deleted the security/audit-fixes branch March 17, 2026 01:33
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