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 16.2.6. 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. Resolved — project is now on Next.js 16.
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 16.2.6. 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 the automated security QA suite (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: 776 checks across 49 suites + 20 negative validation tests.