Skip to content

Commit f05b1ec

Browse files
committed
Address review feedback
- Remove blanket trust for the shared opengeos.org preview origin. - Restrict Cloudflare previews to one label, HTTPS, and the default port. - Make custom ALLOWED_ORIGINS settings authoritative over local defaults. - Cover nested, custom-port, shared-host, and configured-origin cases. - Document the exact default preview-origin policy and exclusions.
1 parent a2989bd commit f05b1ec

4 files changed

Lines changed: 65 additions & 18 deletions

File tree

docs/collaboration.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,11 @@ ephemeral and never written to a project file.
259259
> `Referer`) against `ALLOWED_ORIGINS` via `isAllowedOrigin` (defaults to the
260260
> hosted origins (`geolibre.app`, `web.geolibre.app`, its legacy
261261
> `viewer.geolibre.app` alias, `studio.geolibre.app`, and
262-
> `collab.geolibre.app`), the project-owned Cloudflare/GitHub Pages preview
263-
> origins, loopback hosts (`localhost` and `127.0.0.1`), and
264-
> `tauri://localhost`) as browser-origin filtering
262+
> `collab.geolibre.app`), single-label HTTPS deployment hosts under
263+
> `*.geolibre-preview.pages.dev`, loopback hosts (`localhost` and
264+
> `127.0.0.1`), and `tauri://localhost`). Nested or custom-port preview hosts
265+
> and look-alike domains are rejected; the shared `opengeos.org` GitHub Pages
266+
> preview origin is deliberately not trusted) as browser-origin filtering
265267
> and defense-in-depth (not authentication or a general server-side access gate)
266268
> and enforces a per-IP `checkRateLimit` (10 requests / 60 s). `Access-Control-Allow-Origin: *` is
267269
> still sent on responses so non-browser clients (e.g. Tauri) are not blocked by

workers/collab-node/src/server.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,18 @@ function isAllowedOrigin(
7777
try {
7878
const originUrl = new URL(originHeader);
7979
const host = originUrl.hostname;
80-
if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".localhost")) return true;
81-
if (
82-
!envAllowed &&
83-
originUrl.protocol === "https:" &&
84-
(host === "opengeos.org" || host.endsWith(".geolibre-preview.pages.dev"))
85-
) {
86-
return true;
80+
if (!envAllowed) {
81+
if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".localhost")) return true;
82+
const previewSuffix = ".geolibre-preview.pages.dev";
83+
const previewLabel = host.endsWith(previewSuffix) ? host.slice(0, -previewSuffix.length) : "";
84+
if (
85+
originUrl.protocol === "https:" &&
86+
!originUrl.port &&
87+
previewLabel &&
88+
!previewLabel.includes(".")
89+
) {
90+
return true;
91+
}
8792
}
8893
return allowedList.some((allowed) => {
8994
if (allowed === "*") return true;

workers/collab-node/test/relay.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ describe("Node collaboration relay", () => {
109109
"https://web.geolibre.app",
110110
"https://viewer.geolibre.app",
111111
"https://studio.geolibre.app",
112-
"https://opengeos.org",
113112
"https://50e58010.geolibre-preview.pages.dev",
114113
]) {
115114
const response = await fetch(`${http}/sessions`, {
@@ -130,6 +129,42 @@ describe("Node collaboration relay", () => {
130129
headers: { origin: "https://preview.geolibre-preview.pages.dev.example.com" },
131130
});
132131
assert.equal(rejectedPreviewLookalike.status, 403);
132+
133+
for (const origin of [
134+
"https://opengeos.org",
135+
"https://a.b.geolibre-preview.pages.dev",
136+
"https://preview.geolibre-preview.pages.dev:8443",
137+
]) {
138+
const response = await fetch(`${http}/sessions`, {
139+
method: "POST",
140+
headers: { origin },
141+
});
142+
assert.equal(response.status, 403, `${origin} should be rejected`);
143+
}
144+
});
145+
146+
it("makes a configured origin allowlist authoritative", async () => {
147+
const { http } = await start();
148+
const previous = process.env.ALLOWED_ORIGINS;
149+
process.env.ALLOWED_ORIGINS = "https://allowed.example";
150+
try {
151+
const allowed = await fetch(`${http}/sessions`, {
152+
method: "POST",
153+
headers: { origin: "https://allowed.example" },
154+
});
155+
assert.equal(allowed.status, 200);
156+
157+
for (const origin of ["http://localhost:5173", "https://pr-1.geolibre-preview.pages.dev"]) {
158+
const response = await fetch(`${http}/sessions`, {
159+
method: "POST",
160+
headers: { origin },
161+
});
162+
assert.equal(response.status, 403, `${origin} should require explicit configuration`);
163+
}
164+
} finally {
165+
if (previous === undefined) delete process.env.ALLOWED_ORIGINS;
166+
else process.env.ALLOWED_ORIGINS = previous;
167+
}
133168
});
134169

135170
it("rejects an oversized session-create body by declared length and by count", async () => {

workers/collab/src/index.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,18 @@ function isAllowedOrigin(originHeader: string | null, envAllowed?: string): bool
6363
try {
6464
const originUrl = new URL(originHeader);
6565
const host = originUrl.hostname;
66-
if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".localhost")) return true;
67-
if (
68-
!envAllowed &&
69-
originUrl.protocol === "https:" &&
70-
(host === "opengeos.org" || host.endsWith(".geolibre-preview.pages.dev"))
71-
) {
72-
return true;
66+
if (!envAllowed) {
67+
if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".localhost")) return true;
68+
const previewSuffix = ".geolibre-preview.pages.dev";
69+
const previewLabel = host.endsWith(previewSuffix) ? host.slice(0, -previewSuffix.length) : "";
70+
if (
71+
originUrl.protocol === "https:" &&
72+
!originUrl.port &&
73+
previewLabel &&
74+
!previewLabel.includes(".")
75+
) {
76+
return true;
77+
}
7378
}
7479
return allowedList.some((allowed) => {
7580
if (allowed === "*") return true;

0 commit comments

Comments
 (0)