openvibers/openvibe.reviews Commits

Reviews: fixes from the pre-launch security review (#22)

OP OpenVibers committed 9 Oct 2026 · c6ac620073 · 2 files, +13 −3

- nginx: the sitemaps, feeds and llms-full.txt (each reads every published entity and runs its indexability gate)
  get a per-address limit of 30 a minute, and pages and search 10 a second; only posts were limited before.
- nginx: /metrics and /internal/ are refused case-insensitively (Express routes /METRICS and /Internal/ too).
- The entities API caps offset at 100000.

Changes

ddiff --git a/deploy/nginx/openvibe.reviews.conf b/deploy/nginx/openvibe.reviews.conf
iindex 8795d15..922515f 100644
---- a/deploy/nginx/openvibe.reviews.conf
++++ b/deploy/nginx/openvibe.reviews.conf
@@@ -16,6 +16,9 @@ limit_req_zone $binary_remote_addr zone=ovreviews_auth:10m rate=10r/m;
# Form posts only (reads are not limited here): an empty key is not counted.
map $request_method $ovreviews_write_key { default ""; POST $binary_remote_addr; }
limit_req_zone $ovreviews_write_key zone=ovreviews_write:10m rate=30r/m;
++# Pages and search per address; the sitemaps, feeds and llms-full.txt (each reads every published entity) slower.
++limit_req_zone $binary_remote_addr zone=ovreviews_pages:10m rate=10r/s;
++limit_req_zone $binary_remote_addr zone=ovreviews_index:10m rate=30r/m;
# ── HTTP → HTTPS redirect ──────────────────────────────────
server {
@@@ -82,9 +85,15 @@ server {
proxy_cache_bypass 1;
# Prometheus metrics are for the host only (the app also refuses non-loopback callers).
-- location = /metrics { return 404; }
++ # Case-insensitive, as Express routes /METRICS and /Internal/ like the lowercase paths.
++ location ~* ^/metrics(/|$) { return 404; }
# OpenVibe.Events delivers sources.item.* to 127.0.0.1:4830/internal/events directly; never public.
-- location /internal/ { return 404; }
++ location ~* ^/internal(/|$) { return 404; }
++
++ location ~* ^/(sitemap\.xml|sitemaps/|llms-full\.txt|feed\.(atom|json)) {
++ limit_req zone=ovreviews_index burst=10 nodelay;
++ proxy_pass http://127.0.0.1:4830;
++ }
location /auth/ {
limit_req zone=ovreviews_auth burst=10 nodelay;
@@@ -98,6 +107,7 @@ server {
location / {
limit_req zone=ovreviews_write burst=20 nodelay;
++ limit_req zone=ovreviews_pages burst=40 nodelay;
proxy_pass http://127.0.0.1:4830;
}
}
ddiff --git a/server/http/api.js b/server/http/api.js
iindex 6aca10f..9222bfc 100644
---- a/server/http/api.js
++++ b/server/http/api.js
@@@ -76,7 +76,7 @@ function createApi({ svc, viewers, sources, sync, config, log = console, limits
router.get('/entities', guard('reviews.entity.resolve'), R(async (req) => {
const text = String(req.query.q || '').trim();
if (text) return { entities: (await Promise.all((await svc.search(text, { limit: Number(req.query.limit) || 30 })).map(svc.entityView))) };
-- const out = await svc.listEntities({ limit: Number(req.query.limit) || 50, offset: Number(req.query.offset) || 0 });
++ const out = await svc.listEntities({ limit: Number(req.query.limit) || 50, offset: Math.min(Number(req.query.offset) || 0, 100000) });
return { entities: (await Promise.all(out.entities.map(svc.entityView))), total: out.total };
}));
// A lookup, not a write: counted like a read, in the same budget.