openvibers/openvibe.reviews Commits
Reviews: fixes from the pre-launch security review (#22)
Changes
@@@ -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;}}@@@ -76,7 +76,7 @@ function createApi({ svc, viewers, sources, sync, config, log = console, limitsrouter.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.
@@ -16,6 +16,9 @@ limit_req_zone $binary_remote_addr zone=ovreviews_auth:10m rate=10r/m;
@@ -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.
# 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; }
map $request_method $ovreviews_write_key { default ""; POST $binary_remote_addr; }
limit_req_zone $ovreviews_write_key zone=ovreviews_write:10m rate=30r/m;
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 ──────────────────────────────────
# ── HTTP → HTTPS redirect ──────────────────────────────────
server {
server {
@@ -82,9 +85,15 @@ server {
@@ -82,9 +85,15 @@ server {
proxy_cache_bypass 1;
proxy_cache_bypass 1;
# Prometheus metrics are for the host only (the app also refuses non-loopback callers).
# 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.
# 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/ {
location /auth/ {
limit_req zone=ovreviews_auth burst=10 nodelay;
limit_req zone=ovreviews_auth burst=10 nodelay;
@@ -98,6 +107,7 @@ server {
@@ -98,6 +107,7 @@ server {
location / {
location / {
limit_req zone=ovreviews_write burst=20 nodelay;
limit_req zone=ovreviews_write burst=20 nodelay;
+ limit_req zone=ovreviews_pages burst=40 nodelay;
proxy_pass http://127.0.0.1:4830;
proxy_pass http://127.0.0.1:4830;
}
}
}
}
@@ -76,7 +76,7 @@ function createApi({ svc, viewers, sources, sync, config, log = console, limits
@@ -76,7 +76,7 @@ function createApi({ svc, viewers, sources, sync, config, log = console, limits
router.get('/entities', guard('reviews.entity.resolve'), R(async (req) => {
router.get('/entities', guard('reviews.entity.resolve'), R(async (req) => {
const text = String(req.query.q || '').trim();
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))) };
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 };
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.
// A lookup, not a write: counted like a read, in the same budget.