Last audited: 2026-02-14 (vulnerability table below may be stale — re-run cd frontend && npm audit --omit=dev to refresh)
Note: The project upgraded to Next.js 15.5.12. The advisories below were originally filed against Next.js 14.x. Run
npm audit --omit=devto verify which, if any, still apply to the current version.
| Package | Severity | Advisory | Status |
|---|---|---|---|
next 14.2.35 |
High | GHSA-9g9p-9gw9-jx7f (Image Optimizer DoS) | Accepted risk |
next 14.2.35 |
High | GHSA-h25m-26qc-wcjf (RSC deserialization) | Accepted risk |
glob 10.3.10 |
High | GHSA-5j98-mcp5-4vw2 (CLI injection) | Accepted risk |
@next/eslint-plugin |
High | Transitive via glob |
Accepted risk |
GHSA-9g9p-9gw9-jx7f — Image Optimizer DoS:
- Affects self-hosted Next.js with
remotePatternsin image config. - We deploy on Vercel (managed infrastructure), not self-hosted.
- Our
next.config.jsdoes not configureremotePatterns. - Not practically exploitable in this deployment.
GHSA-h25m-26qc-wcjf — RSC deserialization DoS:
- Requires "insecure React Server Components" usage patterns.
- Our RSC usage is standard (data fetching via Supabase client).
- Vercel's infrastructure provides additional request-level protections.
- Low practical risk. Will be resolved on Next.js 15/16 upgrade.
GHSA-5j98-mcp5-4vw2 — glob CLI injection:
- The
globCLI (--cmdflag) allows command injection. - This is a dev/build-time dependency (via
eslint-config-next). - Never exposed to user input at runtime.
- Not exploitable — only runs during development/CI builds with trusted input.
The project is now on Next.js 15.5.12. The advisories listed above were filed
against v14.x and may no longer apply. Re-run npm audit --omit=dev and refresh
this table when vulnerabilities change.
- Row Level Security (RLS): All Supabase tables have RLS enabled.
- SECURITY DEFINER functions: All 10 API RPCs use
SECURITY DEFINERwithanon_can_execute = false. - Auth middleware: All
/app/*routes require authenticated sessions. - Open redirect prevention: Login redirect param validated (relative paths only, no
//prefix). - No hardcoded secrets: All credentials via environment variables.
This is a public repository by design. Source visibility is expected.
- Code license: AGPL-3.0 in
LICENSE. - Data license: CC BY-NC-SA 4.0 in
DATA_LICENSE.md. - Operational security model: Secrets are never committed; production secrets are managed via environment variables and CI/provider secret stores.
- Abuse resistance: RLS + RPC-only data access + rate limits and query guardrails.
- Schema, migrations, and implementation details are intentionally visible.
- Competitive protection comes from licensing terms and operational execution, not code secrecy.
- Any accidental secret disclosure must be treated as an incident: rotate keys, purge from history, and document remediation.
TryVit is a public food health scoring platform — there is no user-generated content, no PII, and no authentication-gated data. The primary security concerns are:
| Threat | Mitigation |
|---|---|
| Unauthorized data mutation | RLS enabled + FORCE on all tables; write policies only on user_preferences (scoped to auth.uid()); anon is read-only |
| Schema/data exfiltration | Raw table SELECT revoked from anon and authenticated; all data served via SECURITY DEFINER RPCs |
| SQL injection via RPC args | All API functions use parameterized queries (no dynamic SQL with user input in api_product_detail, api_search_products, etc.) |
| Function privilege escalation | Internal functions (compute_*, find_*, refresh_*, cross_validate_*, resolve_effective_country) are revoked from anon/authenticated/PUBLIC |
| Denial of service (query) | statement_timeout = 5s on anon, authenticated, authenticator; idle_in_transaction_session_timeout = 30s |
| Unbounded result sets | All list/search APIs clamp p_limit to max 100; max_rows = 1000 in PostgREST config |
| Stale materialized views | mv_staleness_check() alerts when views exceed refresh threshold |
┌─────────────────────────────────────────────────────┐
│ PostgREST (runs as `authenticator` → sets `anon`) │
├─────────────────────────────────────────────────────┤
│ │
│ anon + authenticated (shared) │
│ ✓ EXECUTE api_product_detail(bigint) │
│ ✓ EXECUTE api_search_products(text, ...) │
│ ✓ EXECUTE api_category_listing(text, ...) │
│ ✓ EXECUTE api_product_detail_by_ean(text, ...) │
│ ✓ EXECUTE api_score_explanation(bigint) │
│ ✓ EXECUTE api_better_alternatives(bigint, ...) │
│ ✓ EXECUTE api_data_confidence(bigint) │
│ ✗ SELECT on any table or view │
│ ✗ INSERT / UPDATE / DELETE on data tables │
│ ✗ EXECUTE on internal functions │
│ │
│ authenticated only │
│ ✓ EXECUTE api_get_user_preferences() │
│ ✓ EXECUTE api_set_user_preferences(...) │
│ ✓ INSERT/UPDATE own row in user_preferences │
│ (RLS: auth.uid() = user_id) │
│ │
│ service_role │
│ ✓ Full CRUD on all tables │
│ ✓ Used by data pipelines and admin scripts │
│ │
├─────────────────────────────────────────────────────┤
│ SECURITY DEFINER functions (run as `postgres`) │
│ → Can read all tables/views regardless of │
│ client-role privileges │
│ → All have `SET search_path = public` │
│ (prevents search_path hijacking) │
│ → Note: `postgres` is NOT superuser in Supabase │
│ (rolsuper=false) — relies on explicit grants │
└─────────────────────────────────────────────────────┘
Direct REST access to tables and views is blocked for client-facing roles (anon, authenticated). All data access is routed through nine curated API functions:
| Function | Purpose | Access |
|---|---|---|
api_product_detail |
Full product view with freshness | anon + auth |
api_search_products |
Text search with diet/allergen filter | anon + auth |
api_category_listing |
Browse by category with sort/page | anon + auth |
api_product_detail_by_ean |
Barcode scanner lookup | anon + auth |
api_score_explanation |
Score breakdown with category context | anon + auth |
api_better_alternatives |
Healthier alternatives for a product | anon + auth |
api_data_confidence |
Data quality assessment per product | anon + auth |
api_get_user_preferences |
Retrieve user's saved preferences | auth only |
api_set_user_preferences |
Save country/diet/allergen settings | auth only |
This approach provides:
- Contract stability — API key sets and country-echo contract are locked and tested (33 API contract QA checks)
- Performance control — Functions apply pagination limits and optimized queries
- Security — No direct table access means zero risk of filter bypass or column enumeration
RLS is enabled and forced on all 12 data tables.
Public data tables (11 tables): Policies are SELECT USING (true) — permissive by design since all data is public. These policies serve as defense-in-depth: even if SELECT privilege were accidentally re-granted, RLS would still apply. Write policies (INSERT/UPDATE/DELETE) do not exist, enforcing read-only access.
user_preferences (1 table): User-scoped RLS with auth.uid() = user_id on all operations (SELECT, INSERT, UPDATE, DELETE). Each authenticated user can only access their own row. This is the only table with user-specific write policies.
resolve_effective_country(text) is a SECURITY DEFINER internal helper with SET search_path = public. EXECUTE is revoked from PUBLIC, anon, and authenticated — it can only be called by other SECURITY DEFINER functions (the API layer). This function reads user_preferences to resolve the user's preferred country, and the SECURITY DEFINER attribute ensures this works regardless of the caller's role privileges.
Security posture is validated by 22 automated checks (QA__security_posture.sql):
- All data tables have RLS enabled
- All data tables have FORCE RLS enabled
- Each data table has a SELECT policy
- No write policies exist on public data tables (user_preferences excluded)
anonhas no INSERT privilegeanonhas no UPDATE privilegeanonhas no DELETE privilege- All
api_*functions are SECURITY DEFINER anoncan EXECUTE allapi_*functionsanonblocked from internal functions (incl.resolve_effective_country)service_roleretains full privileges- All
api_*functions havesearch_pathset anonhas no SELECT on data tables (RPC-only)- New tables have RLS enabled
- Products table has
updated_attrigger user_preferenceshas RLS enabled and forceduser_preferenceshas user-scoped SELECT policyuser_preferenceshas user-scoped INSERT policyuser_preferenceshas user-scoped UPDATE policyuser_preferenceshasupdated_attriggerresolve_effective_countryis SECURITY DEFINER withsearch_pathsetresolve_effective_countryEXECUTE revoked fromauthenticated
Total QA coverage: 421 checks across 30 suites + 29 negative validation tests.