Skip to content

Security

Luara edited this page Jul 5, 2026 · 3 revisions

Security

Felines was designed around a unique risk: the product maps locations of vulnerable animals. Security here isn't just data protection — it's physical protection of the cats. This page is especially relevant to Philippe Dourassov (Aikido Security, #1 hacker worldwide 2024), the security judge for this hackathon.

Full audit report: AUDIT_REPORT.md Vulnerability disclosure policy: SECURITY.md


1. Progressive Location Blur

The most important security feature in the product — designed to protect cats, not just user data. Real cases of colonies being targeted after their locations were shared carelessly online informed this design.

Level User Precision Offset Enforced by
1 Anonymous Neighborhood ~500m + visible uncertainty circle latitude_blurred/longitude_blurred — the only columns anon has a grant on
2 Authenticated, not caretaker Street level ~100m latitude_blurred_near/longitude_blurred_near — the only additional columns authenticated has a grant on
3 Linked caretaker or creator Exact none get_colony_exact_location(p_colony_id) — re-checks the caretaker/creator link against the database on every call

Implementation:

revoke select (latitude, longitude) on colonies from anon, authenticated;

Even a signed-in caretaker's own browser cannot fetch those two columns directly — the only path to the real value is the RPC above, which never trusts a cached session flag or a client-side boolean.

Tested live: SELECT latitude FROM colonies with the anon key → 42501 permission denied ✅. The identical column-vs-function pattern is reused for reports.latitude/longitude.


2. Row Level Security — 26 Tables

RLS enabled on all 26 tables. Every policy was tested with direct API calls using the anon key — the same vantage point an attacker with no account has.

Table Anonymous Authenticated Caretaker / Owner
colonies Blurred columns only Blurred + near-blurred columns Exact coordinates via RPC
reports INSERT only (no SELECT) INSERT + SELECT SELECT + UPDATE (own or caretaker's colony)
knowledge_progress No access Own rows only Own rows only
profiles Public fields only Own full row + others' public fields Same
flags INSERT only INSERT only INSERT only
notifications No access Own rows only (select/update/delete) Same

3. Secure RPCs

Every RPC that writes data on behalf of another user, or makes an authorization decision a row policy can't express, is SECURITY DEFINER.

A real vulnerability found during development: several RPCs were callable by the anon key by default Supabase privilege — record_daily_visit was found still callable anonymously during a live test despite its migration's own comment claiming otherwise (the deployed database had drifted from the migration history). Fixed by 0075_reassert_record_daily_visit_grant.sql, then re-verified live.

RPC Protection Migration
get_colony_exact_location Caretaker/creator link re-validated server-side, every call 0016
confirm_report Atomic increment, blocks self-confirmation and duplicate confirmation 0038
record_daily_visit Idempotent, anon access re-revoked after drift was found 0064, 0075
thank_action Blocks self-thanking 0068
notify_caretakers / notify_nearby_caretakers Fixed server-side message template, no caller-supplied free text 0065
notify_caretakers (rate limit) Database-level circuit breaker — one notification batch per colony per 5 minutes, even via direct REST calls 0078
is_user_banned SECURITY DEFINER, used inside INSERT policies so ban status can be checked without a public grant 0082

A note on notify_caretakers: the fixed message template wasn't a stylistic choice, it was a security decision. Every granted RPC is exposed as a public REST endpoint (/rest/v1/rpc/notify_caretakers) reachable with the same anon key that ships in every page load — if the message content were caller-supplied, this endpoint would be a ready-made spam/phishing vector.


4. Aikido Security Findings

Path Traversal (HIGH severity) ✅ Fixed

Problem: File uploads across 6 upload sites (colony photos, cat photos, avatars, timeline photos, story photos, the new-colony form) built storage paths from user-influenced input without sanitization — a filename or prefix containing ../ sequences could, in principle, write outside the intended storage folder.

Fix: centralized all path construction in lib/storage.ts:

  • buildSafeStoragePath() strips ..//..\ sequences and any character outside an explicit allowlist
  • The filename is never the user's original filename — it's a timestamp + random token + a validated extension
  • validatePhotoFile() checks an explicit MIME allowlist (image/jpeg, image/png, image/webp, image/gif) instead of a broad image/* match, which would also accept image/svg+xml — SVGs can embed <script> tags, a known upload-based XSS vector
  • assertSafeStoragePath() re-asserts the path can't contain .. or start with /, called again immediately before every .upload() call as defense in depth

Commit: security: fix path traversal vulnerability in file uploads — Aikido report

SSRF in Geocoding (LOW severity) ✅ Fixed

Problem: The reverse-geocoding call to Nominatim interpolated latitude/longitude directly into a URL string with no validation. Severity was rated LOW because coordinates in practice come from Leaflet map clicks, not typed input.

Fix: lib/validateCoordinates.ts (rejects out-of-range, NaN, Infinity, or non-numeric coordinates before any request is built), URLSearchParams instead of string interpolation in lib/geocode.ts, and redirect: "error" so a malicious redirect response can't be followed silently. The same unvalidated pattern was also found in the weather-fetch helper (outside Aikido's original report) and fixed identically, plus a pre-existing bug where the geocoding response's language was always Portuguese regardless of visitor preference.

Commit: security: fix SSRF vulnerability in geocoding requests — Aikido report


5. Security Headers (next.config.ts)

Header Value Protects against
X-DNS-Prefetch-Control on Controls DNS prefetching behavior
X-Frame-Options DENY Clickjacking
X-Content-Type-Options nosniff MIME-type sniffing
Referrer-Policy strict-origin-when-cross-origin Referrer/URL leakage
Permissions-Policy camera=(), microphone=(), geolocation=(self) Unwanted browser permission access
X-XSS-Protection 1; mode=block Legacy XSS fallback
Strict-Transport-Security max-age=63072000; includeSubDomains; preload Downgrade to plain HTTP
Content-Security-Policy Scoped to the app's real external origins XSS, unauthorized resource loading

6. Rate Limiting, in Two Layers

  • API layer: lib/rateLimit.ts, an in-memory sliding-window limiter — 10 requests/hour anonymous, 30/hour authenticated, keyed by IP or user id.
  • Database layer: a circuit breaker inside notify_caretakers() itself, so the limit still holds even if a caller skips the Next.js API route entirely and calls the public Supabase RPC endpoint directly with the anon key.

7. Input Validation, Everywhere, at Both Layers

Client-side maxLength and range checks are a UX nicety — every real boundary is re-enforced by a database CHECK constraint:

  • char_length constraints on every free-text column (colony name, narrative, report description, cat name, caretaker letter, story content, resource description, profile display name, public contact)
  • Geographic range CHECK constraints (-90..90 latitude, -180..180 longitude) on every coordinate column in colonies and reports
  • MIME-type allowlist on every upload path

8. The 3-False-Pin-Flag Ban System

Once a single colony pin accumulates 3 false-pin flags (never_seen_cats, location_doesnt_exist, duplicate_colony, suspicious_harmful), a database trigger (0082) automatically:

  1. Removes the colony pin from the map (colonies.removed_at)
  2. Bars the creator from posting new colonies/reports for 1 month (profiles.banned_until)
  3. Notifies the creator, explaining exactly why and for how long
  4. On the 3rd such ban for the same account, makes the ban permanent (profiles.banned)

This is deliberately distributed moderation — nobody at Felines has to manually review every disputed pin, but a single disgruntled flag also can't take a real colony down; it takes three independent people agreeing something is wrong. Ban status is checked via is_user_banned(), a SECURITY DEFINER function used inside the INSERT policies for colonies and reports, so banned/ban-count columns never need a public grant.


9. Auth Security

  • Open-redirect protection: the post-login/signup returnTo redirect parameter is validated by lib/safeReturnTo.ts to accept only an internal path starting with exactly one / — rejecting //evil.com and any absolute URL.
  • No service-role key exists anywhere in client-side code, server code, .env.local, or git history — confirmed by a full-repository grep.
  • Streak data and ban status (banned, banned_until, ban_count) are not publicly readable — protected the same way exact coordinates are, via column-level grant revocation plus a narrow SECURITY DEFINER accessor function.

10. Animal Safety as a Design Decision

Several product decisions were made specifically to protect the physical safety of the cats — not just user data:

  • No reporting people, only situations — prevents digital vigilantism between neighbors
  • 3-confirmation threshold — prevents one user from hiding or fabricating a serious report
  • Sensitive reports never deleted — permanent evidence trail for poisoning/abuse/disease incidents
  • No public shareable colony URL — even with blur, a direct link lowers the barrier for malicious access
  • UUID filenames — original user input never reaches storage paths
  • Exact coordinates only via server-side RPC — never in any public API response

Security Migrations Reference

Migration Fix
0044–0045 Critical/secondary RPCs reject anon key
0065 Notification RPCs hardened against spam injection
0066 Streaks restricted to own user
0067 Field size limits added to all columns
0068 Self-thanking blocked
0069 Unique constraint on knowledge_progress
0070 Flag deduplication
0075 record_daily_visit anon grant re-revoked after drift
0077 Colony stories restricted to logged-in visibility
0078 notify_caretakers rate-limited at the database layer
0079 Coordinate range CHECK constraints
0080 profiles.display_name length constraint
0082 3-false-pin-flag ban system

Reporting a Vulnerability

See SECURITY.md for the full responsible disclosure policy.

Clone this wiki locally