Skip to content

fix: harden security across API, desktop, and frontend layers#233

Open
bperkins-oss wants to merge 1139 commits intokoala73:mainfrom
bperkins-oss:main
Open

fix: harden security across API, desktop, and frontend layers#233
bperkins-oss wants to merge 1139 commits intokoala73:mainfrom
bperkins-oss:main

Conversation

@bperkins-oss
Copy link

Summary

Security audit identified several vulnerabilities across the API, desktop app, and frontend. This PR fixes 19 issues across 15 files:

Critical/High fixes:

  • Remove Cargo.lock from .gitignore for reproducible Rust builds
  • Add domain allowlist to sidecar RSS proxy (prevents SSRF — was an open proxy)
  • Apply encodeURIComponent to ArXiv proxy params (prevents URL parameter injection)
  • Remove GOPROXY=direct to restore Go checksum verification
  • Add security headers (HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy) to vercel.json and middleware.ts
  • Replace weak PID+timestamp sidecar token with CSPRNG (/dev/urandom)

Medium fixes:

  • Fix CORS wildcard fallback and version endpoint to use explicit origin
  • Remove overly broad Vercel preview CORS pattern from YouTube embed
  • Validate YouTube channel parameter with strict regex
  • Validate download redirect URL starts with https://github.com/
  • Remove style from safeHtml() attribute allowlist (prevents CSS injection)
  • Redact apiDir filesystem path from sidecar status endpoint

Low fixes:

  • Fix postMessage wildcard origin in YouTube embed → use validated parent origin
  • Add origin validation to embed message event listener
  • Replace inline onclick with addEventListener (CSP-compatible)
  • Escape e.method in traffic log table (XSS defense-in-depth)

Test plan

  • Verify RSS feeds load correctly in both web and desktop app
  • Verify YouTube live streams and embed playback still work
  • Verify ArXiv paper search returns results
  • Verify desktop app sidecar starts and authenticates correctly
  • Verify download redirect works for all platforms
  • Check security headers are present via curl -I https://worldmonitor.app
  • Verify safeHtml() tooltip rendering still works without style attribute

🤖 Generated with Claude Code

koala73 and others added 30 commits February 17, 2026 16:02
Polymarket and other proxied requests from finance variant were blocked
by CORS because the Railway relay only allowed worldmonitor.app and
tech.worldmonitor.app origins.
Switch from CARTO dark_all raster tiles (which showed continent names
in local languages like 亚洲, AFRIKA, أفريقيا) to CARTO Dark Matter
vector style with English labels. Set renderWorldCopies: false to
prevent horizontal map duplication. Use interleaved deck.gl overlay
so basemap labels render above data layers.

Closes koala73#81

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Closes koala73#90

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add bottom viewport clamp so popups near the map's bottom edge slide
upward to remain fully visible instead of being cut off.
setStyle() replaces all map sources/layers. Country boundaries were
only loaded once (guarded by countryGeoJsonLoaded) so they vanished
after theme toggle with no way to re-add them.

- Reset countryGeoJsonLoaded in switchBasemap so loadCountryBoundaries
  can re-run after the new style loads
- Listen for style.load before re-adding country source/layers
- Guard setupCountryHover with countryHoverSetup flag to prevent
  duplicate mousemove/mouseout listeners on re-load
- Apply theme-correct paint values after layer creation
Store the active highlighted ISO code so it can be re-applied after
setStyle() rebuilds map layers with empty default filters.
WebGL renderer check now also detects Intel GPUs to return macos-x64.
When architecture can't be determined, return unknown so users see
both Apple Silicon and Intel download buttons instead of defaulting
to the wrong binary.
Add 'macos' platform type for Macs where WebGL can't determine
Apple Silicon vs Intel. Shows both Mac download buttons without
the irrelevant Windows option.
## Summary
- Detects user's OS via `navigator.userAgent` and shows only the
relevant download button (Windows, macOS Apple Silicon, or macOS Intel)
- Uses WebGL renderer info to distinguish Apple Silicon from Intel Macs
where possible
- Adds a "Show all platforms" toggle so users can still access other
platform downloads
- Falls back to showing all 3 buttons if OS can't be detected

## Test plan
- [x] Open the web app on a Mac — should see only the macOS (Apple
Silicon) button
- [x] Open on Windows — should see only the Windows button
- [x] Click "Show all platforms" — all 3 buttons should appear
- [x] Click "Show less" — should collapse back to the detected platform
- [x] Spoof an unrecognized user agent — all 3 buttons should show (no
toggle)

Replaces koala73#91
Closes koala73#90

🤖 Generated with [Claude Code](https://claude.com/claude-code)
- Guard against Invalid Date in RSS parser — malformed pubDate strings
  from IAEA/CrisisWatch feeds caused RangeError on toISOString()
- Replace 13 dead/blocked RSS feed URLs (403/404/500) with Google News
  site-scoped fallbacks: Politico, RUSI, Kyiv Independent, War Zone,
  MEI, Wilson Center, GMF, CNAS, Lowy, Arms Control, Bulletin, EU ISS
…backs

Sequoia Blog, EU Startups, Tech in Asia, LAVCA, YC Launches,
Dev Events, and SemiAnalysis were returning 403/404/timeout errors.
…eeds

- Middle East panel: BBC Persian (direct RSS), Iran International and
  Fars News (Google News fallbacks — no public RSS endpoints)
- Asia panel: MIIT and MOFCOM China government feeds via RSSHub
Allow users to hide the Intelligence Findings badge for passive viewing
(e.g. TV displays). The badge can be disabled via right-click context
menu or the PANELS settings modal. Preference persists in localStorage.

Closes koala73#89

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
- Adds ability to hide the Intelligence Findings badge for passive/TV
viewing — stops polling, sounds, and pulse animations when disabled
- Two ways to toggle: right-click context menu on the badge, or the
PANELS settings modal
- Preference persists in `localStorage` across page reloads

Closes koala73#89

## Test plan
- [ ] Load app — badge shows by default (no regression)
- [ ] Right-click badge → "Hide Intelligence Findings" → badge
disappears, no polling/sounds
- [ ] Open PANELS → "Intelligence Findings" toggle shows as disabled →
click to re-enable → badge reappears
- [ ] Refresh page → preference persists
- [ ] Build succeeds with no type errors

🤖 Generated with [Claude Code](https://claude.com/claude-code)
These CSS custom properties were used in 14 places but never defined,
causing transparent backgrounds on playback panel, toggle button,
header flash animation, select options, and offline retry button.
"here" and other basic English words (pronouns, prepositions, adverbs)
were not in SUPPRESSED_TRENDING_TERMS, causing false keyword spike
findings for common words.
"here" and other basic English words (pronouns, prepositions, adverbs)
were not in SUPPRESSED_TRENDING_TERMS, causing false keyword spike
findings for common words.
…ages

Audit found missing keys in all non-EN locales (62-90 per file) and 25
stale keys in PT/NL/SV. Fixes gaps, removes stale keys, corrects FR
infra key misplacement, and adds complete Russian (ru) translation with
1013/1013 keys matching the English reference.
koala73 and others added 25 commits February 21, 2026 09:02
Bump s-maxage from 300s to 600s and stale-while-revalidate from 60s to
300s. Client already caches feeds for 10 min locally, so users see no
freshness difference while Vercel edge serves cached responses longer.
…73#215)

* feat: API key gating for desktop cloud fallback + registration system

Gate desktop cloud fallback behind WORLDMONITOR_API_KEY — desktop users
need a valid key for cloud access, otherwise operate local-only (sidecar).
Add email registration system via Convex DB for future key distribution.

Client-side: installRuntimeFetchPatch() checks key presence before
allowing cloud fallback, with secretsReady promise + 2s timeout.
Server-side: origin-aware validation in sebuf gateway — desktop origins
require key, web origins pass through.

- Add WORLDMONITOR_API_KEY to 3-place secret system (Rust, TS, sidecar)
- New "World Monitor" settings tab with key input + registration form
- New api/_api-key.js server-side validation (origin-aware)
- New api/register-interest.js edge function with rate limiting
- Convex DB schema + mutation for email registration storage
- CORS headers updated for X-WorldMonitor-Key + Authorization
- E2E tests for key gate (blocked without key, allowed with key)
- Deployment docs (API_KEY_DEPLOYMENT.md) + updated desktop config docs

* fix: harden worldmonitor key + registration input handling

* fix: show invalid WorldMonitor API key status

* fix: simplify key validation, trim registration checks, add env example vars

- Inline getValidKeys() in _api-key.js
- Remove redundant type checks in register-interest.js
- Simplify WorldMonitorTab status to present/missing
- Add WORLDMONITOR_VALID_KEYS and CONVEX_URL to .env.example

* feat(sidecar): integrate proto gateway bundle into desktop build

The sidecar's buildRouteTable() only discovers .js files, so the proto
gateway at api/[domain]/v1/[rpc].ts was invisible — all 45 sebuf RPCs
returned 404 in the desktop app. Wire the existing build script into
Tauri's build commands and add esbuild as an explicit devDependency.
Rebuild the World Monitor settings tab with hero banner, license key
input, waitlist registration, and BYOK footer. Only validate API key
panels that have pending changes on save. Add local RSS proxy handler
to sidecar so desktop fetches feeds directly without cloud fallback.
Bump version to 2.5.3.
- Move World Monitor tab to first position in settings.html
- Add registration proxy in sidecar to bypass Vercel bot protection
- Fix sidecar RSS/registration handlers to use response.text()
- Skip empty values in loadDesktopSecrets (NO LICENSE vs LICENSED)
- Add skip-setup text to desktop config alert panel
- Sidecar calls Convex HTTP API directly (Vercel Attack Challenge Mode
  blocks server-side proxy). CONVEX_URL read from env, not hardcoded.
- Rust injects CONVEX_URL into sidecar via option_env! (CI) / env var (dev)
- GitHub Actions passes CONVEX_URL secret to all 4 build steps
- Tighten WM tab CSS spacing so all content fits in one viewport
…koala73#218)

Restore the WORLDMONITOR_API_KEY check that was removed in e882a00,
which left desktop cloud fallback ungated — causing deterministic 401s
from the edge gateway for keyless desktop installs. Also disable cloud
fallback when the runtime-config module fails to import, since the
cloudFallback() path depends on the same module and would throw.

https://claude.ai/code/session_014yJsGsxD1sWt6B6PvQXiaA

Co-authored-by: Claude <noreply@anthropic.com>
* feat: add cable health scoring via sebuf InfrastructureService

Port submarine cable health monitoring from PR koala73#134 to the sebuf
architecture. Adds GetCableHealth RPC to InfrastructureService that
analyzes NGA maritime warnings to detect cable faults and repair
activity, computing health scores with time-decay.

- Proto: GetCableHealthRequest/Response, CableHealthRecord, evidence
- Handler: NGA warning fetch, cable matching (name + proximity), signal
  processing, health computation with redis caching
- Client: circuit breaker, proto enum → frontend string adapter, 1-min cache
- Frontend: health-based cable coloring (fault=red, degraded=orange),
  evidence display in cable popup, SVG + DeckGL support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review feedback for cable health scoring

- Fix geographic proximity: use cosine-latitude correction instead of
  raw Euclidean distance on lat/lon degrees, return distanceKm directly
- Fix signal kind: use 'cable_advisory' (not 'operator_fault') for
  non-fault NGA warnings so advisories don't trigger fault status
- Parallelize loadCableActivity + loadCableHealth with Promise.all
- Remove console.log from client-side cable-health service
- Add in-memory fallback cache on server so transient Redis+NGA
  failures serve stale data instead of empty response

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
PR koala73#220 added popups.cable.health.evidence only to en.json.
Add translated values to all 15 other locales.
…back

Prevent timeout spam on Railway when UCDP API is down:
- Negative cache: 5-min backoff after upstream failure
- Version discovery cache: reuse discovered API version for 1 hour
- Parallel version probing via Promise.allSettled
- Stale-on-error: serve fallback data instead of empty array
Plaintext keys (OLLAMA_API_URL, OLLAMA_MODEL, etc.) render pre-filled
with stored values. captureUnsavedInputs() was capturing these unchanged
values into pendingSecrets, triggering unnecessary verification on save.
Now compares against stored value and skips if unchanged.
- Add /ingest/* rewrites in vercel.json → us.i.posthog.com
- Web uses /ingest proxy, desktop uses direct PostHog host
- Enable capture_pageview so every visitor registers
- Fix proto.price falsy bug: price of 0 was treated as null
- Replace global lastSuccessfulResults with per-symbol-set Map to prevent
  stock data leaking into commodity fallback
- Add yahooGate (600ms) to serialize Yahoo requests and avoid IP rate limits
- Add per-symbol-set cache key in server handler to isolate stock/commodity/sector calls
- Clear UCDP circuit breaker cache on empty responses to prevent 10-min lockout
- Add UCDP retry loop (3 attempts, 15s apart) on cold start
- Delay ETF panel initial fetch by 8s to reduce Yahoo contention on startup
When user has a WorldMonitor API key, enable cloud relay through
worldmonitor.app as fallback for Yahoo 429 rate limits.
## Changelog

### Bug Fixes
- market: Fix price falsy bug (price of 0 treated as null)
- market: Per-symbol-set caching prevents stock/commodity data leakage
- market: Yahoo request gate (600ms) reduces IP-level rate limiting
- market: ETF panel 8s delayed fetch reduces Yahoo contention on startup
- ucdp: Clear circuit breaker cache on empty responses
- ucdp: Retry loop (3 attempts, 15s) for cold start resilience
- ucdp: Negative cache, version cache, stale-on-error fallback
- analytics: Proxy PostHog through own domain to bypass ad blockers
- settings: Skip API key re-verification when no keys changed
- csp: Allow PostHog scripts from us-assets.i.posthog.com
- api: Sanitize og-story level input
- api: Restore API-key gate on config import failure

### Features
- Cable health scoring via sebuf InfrastructureService
- PostHog analytics with privacy-first design

### i18n
- Cable health evidence key added to all locales
Invert the path logic from an allowlist of watched directories to an
exclusion list (*.md, .planning, docs, e2e, scripts, .github). This
brings the command from 318 to 242 characters while keeping the same
build-trigger behavior for all source code changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ty) (koala73#226)

* docs: add community guidelines — contributing guide, code of conduct, and security policy

Add three community health files for the open-source project:

- CONTRIBUTING.md: comprehensive guide covering architecture overview (sebuf,
  variants, directory structure), development setup with make commands,
  AI-assisted development policy, sebuf RPC workflow, data source and RSS
  feed contribution guides, coding standards, and PR process
- CODE_OF_CONDUCT.md: Contributor Covenant v2.1 adapted for World Monitor
- SECURITY.md: responsible disclosure policy, security considerations for
  edge functions/sebuf handlers, and contributor best practices

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add missing blank line before list in CONTRIBUTING.md (MD032)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: expand AI section with LLM label attribution and rationale

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: remove GitHub link from AI section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: simplify AI section back to concise version with PR labels

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Remove Cargo.lock from .gitignore for reproducible Rust builds
- Add domain allowlist to sidecar RSS proxy (prevents SSRF)
- Apply encodeURIComponent to ArXiv proxy params (prevents injection)
- Remove GOPROXY=direct to restore Go checksum verification
- Add security headers (HSTS, X-Frame-Options, X-Content-Type-Options)
- Replace weak PID+timestamp token with CSPRNG (/dev/urandom)
- Fix CORS wildcard fallback and version endpoint to use explicit origin
- Remove overly broad Vercel preview CORS pattern from YouTube embed
- Validate YouTube channel param with strict regex
- Validate download redirect URL is on github.com
- Fix postMessage wildcard origin in YouTube embed
- Add origin validation to embed message listener
- Remove style from safeHtml allowlist (prevents CSS injection)
- Replace inline onclick with addEventListener (CSP-compatible)
- Escape e.method in traffic log table (XSS defense-in-depth)
- Redact apiDir from sidecar status endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Feb 22, 2026

@bperkins-oss is attempting to deploy a commit to the Elie Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Owner

@koala73 koala73 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security Hardening Review — 3 Blocking Issues

Most fixes are correct and close real vulnerabilities. However, there are critical problems that will break functionality.


BLOCKING

1. X-Frame-Options: DENY breaks YouTube embed iframe
vercel.json applies X-Frame-Options: DENY to /(.*) which includes /api/youtube/embed — the HTML page loaded in an iframe by LiveNewsPanel. Browsers will refuse to render it. Either use SAMEORIGIN globally or exclude the embed route from the header.

2. ArXiv encoding will likely break search
list-arxiv-papers.ts now does:

searchQuery = `all:${encodeURIComponent(req.query)}+AND+cat:${encodeURIComponent(category)}`;

The +AND+ is an ArXiv query operator mixed with half-encoded components. Use URLSearchParams to encode the full search_query parameter properly instead:

const params = new URLSearchParams({
  search_query: searchQuery,
  start: '0',
  max_results: String(pageSize),
});
const url = `https://export.arxiv.org/api/query?${params}`;

3. Security headers in middleware are dead code
SECURITY_HEADERS in middleware.ts is only spread into the 403 bot-block responses. Normal requests pass through without them. The vercel.json headers cover real traffic, so the middleware code gives a false sense of coverage. Remove it from middleware (rely on vercel.json) or apply via NextResponse.next().


SUGGESTIONS

4. Token generation doesn't use CSPRNG on Windows
main.rs reads /dev/urandom which doesn't exist on Windows. Fallback is still the weak PID+timestamp hash. Use getrandom::getrandom(&mut buf) for cross-platform CSPRNG — it's already in Rust's dependency tree.

5. RSS allowlist is duplicated (~100 lines)
Sidecar now has a copy-pasted RSS_ALLOWED_DOMAINS that mirrors api/rss-proxy.js. No mechanism keeps them in sync. Extract to a shared JSON file or at minimum add "KEEP IN SYNC with api/rss-proxy.js" comments in both files.

6. showEmbedError still uses inline patterns
PR fixes showOfflineMessage to use addEventListener but showEmbedError in the same file still uses innerHTML + template literal pattern. Inconsistent.

7. e.status and e.durationMs not escaped in settings-main.ts
Same defense-in-depth reasoning that motivated escaping e.method applies to these numeric fields — coerce with ${Number(e.status)} or escapeHtml().


Good Changes (no issues)

  • CORS wildcard → explicit origin in catch blocks ✓
  • Sidecar RSS domain allowlist (closes SSRF) ✓
  • YouTube postMessage wildcard → validated origin ✓
  • YouTube channel parameter validation ✓
  • Download redirect URL validation ✓
  • Removing GOPROXY=direct
  • Removing apiDir from sidecar status ✓
  • Committing Cargo.lock
  • Removing style from safeHtml allowlist ✓
  • Inline onclickaddEventListener
  • Escaping e.method

Blocking fixes:
- Use X-Frame-Options SAMEORIGIN instead of DENY to avoid breaking
  YouTube embed iframe in LiveNewsPanel
- Replace manual encodeURIComponent with URLSearchParams for ArXiv
  query encoding to avoid breaking +AND+ operators
- Remove dead SECURITY_HEADERS from middleware.ts (only applied to
  403 bot-block responses, vercel.json covers real traffic)

Suggestions:
- Use getrandom crate for cross-platform CSPRNG token generation
  instead of /dev/urandom with weak PID+timestamp fallback
- Extract RSS allowed domains to shared data/rss-allowed-domains.json
  read by both api/rss-proxy.js and sidecar, eliminating duplication
- Set showEmbedError href programmatically instead of interpolating
  into innerHTML template (consistent with showOfflineMessage fix)
- Coerce e.status and e.durationMs with Number() in traffic log table

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

10 participants