Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/collaboration.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,13 @@ ephemeral and never written to a project file.

> **Operator note:** `POST /sessions` validates the request `Origin` (or
> `Referer`) against `ALLOWED_ORIGINS` via `isAllowedOrigin` (defaults to the
> app's own domains plus `localhost` for development) as browser-origin filtering
> hosted origins (`geolibre.app`, `web.geolibre.app`, its legacy
> `viewer.geolibre.app` alias, `studio.geolibre.app`, and
> `collab.geolibre.app`), single-label HTTPS deployment hosts under
> `*.geolibre-preview.pages.dev`, loopback hosts (`localhost` and
> `127.0.0.1`), and `tauri://localhost`). Nested or custom-port preview hosts
> and look-alike domains are rejected; the shared `opengeos.org` GitHub Pages

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unbalanced parentheses: the outer (defaults to the hosted origins …) parenthetical already closes right after `tauri://localhost`). The ) at the end of this line has no matching (, so the sentence reads oddly (… not trusted) as browser-origin filtering). Confidence: high (verified by counting the parens through this whole note).

Suggested change
> and look-alike domains are rejected; the shared `opengeos.org` GitHub Pages
> preview origin is deliberately not trusted as browser-origin filtering

> preview origin is deliberately not trusted) as browser-origin filtering
> and defense-in-depth (not authentication or a general server-side access gate)
> and enforces a per-IP `checkRateLimit` (10 requests / 60 s). `Access-Control-Allow-Origin: *` is
> still sent on responses so non-browser clients (e.g. Tauri) are not blocked by
Expand Down
17 changes: 16 additions & 1 deletion workers/collab-node/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ function isAllowedOrigin(
.filter(Boolean)
: [
"https://geolibre.app",
"https://web.geolibre.app",
"https://viewer.geolibre.app",
"https://studio.geolibre.app",
"https://collab.geolibre.app",
"http://localhost",
"http://127.0.0.1",
Expand All @@ -74,7 +77,19 @@ function isAllowedOrigin(
try {
const originUrl = new URL(originHeader);
const host = originUrl.hostname;
if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".localhost")) return true;
if (!envAllowed) {
if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".localhost")) return true;
const previewSuffix = ".geolibre-preview.pages.dev";
const previewLabel = host.endsWith(previewSuffix) ? host.slice(0, -previewSuffix.length) : "";
if (
originUrl.protocol === "https:" &&
!originUrl.port &&
previewLabel &&
!previewLabel.includes(".")
) {
return true;
}
}
return allowedList.some((allowed) => {
if (allowed === "*") return true;
try {
Expand Down
66 changes: 66 additions & 0 deletions workers/collab-node/test/relay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,72 @@ describe("Node collaboration relay", () => {
await assert.rejects(connect(http, "NOTFOUND"), /Unexpected server response: 404/);
});

it("allows the hosted GeoLibre web origins to create sessions", async () => {
const { http } = await start();

for (const origin of [
"https://geolibre.app",
"https://web.geolibre.app",
"https://viewer.geolibre.app",
"https://studio.geolibre.app",
"https://50e58010.geolibre-preview.pages.dev",
]) {
const response = await fetch(`${http}/sessions`, {
method: "POST",
headers: { origin },
});
assert.equal(response.status, 200, `${origin} should be allowed`);
}

const rejected = await fetch(`${http}/sessions`, {
method: "POST",
headers: { origin: "https://web.geolibre.app.example.com" },
});
assert.equal(rejected.status, 403);

const rejectedPreviewLookalike = await fetch(`${http}/sessions`, {
method: "POST",
headers: { origin: "https://preview.geolibre-preview.pages.dev.example.com" },
});
assert.equal(rejectedPreviewLookalike.status, 403);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

for (const origin of [
"https://opengeos.org",
"https://a.b.geolibre-preview.pages.dev",
"https://preview.geolibre-preview.pages.dev:8443",
]) {
const response = await fetch(`${http}/sessions`, {
method: "POST",
headers: { origin },
});
assert.equal(response.status, 403, `${origin} should be rejected`);
}
});

it("makes a configured origin allowlist authoritative", async () => {
const { http } = await start();
const previous = process.env.ALLOWED_ORIGINS;
process.env.ALLOWED_ORIGINS = "https://allowed.example";
try {
const allowed = await fetch(`${http}/sessions`, {
method: "POST",
headers: { origin: "https://allowed.example" },
});
assert.equal(allowed.status, 200);

for (const origin of ["http://localhost:5173", "https://pr-1.geolibre-preview.pages.dev"]) {
const response = await fetch(`${http}/sessions`, {
method: "POST",
headers: { origin },
});
assert.equal(response.status, 403, `${origin} should require explicit configuration`);
}
} finally {
if (previous === undefined) delete process.env.ALLOWED_ORIGINS;
else process.env.ALLOWED_ORIGINS = previous;
}
});

it("rejects an oversized session-create body by declared length and by count", async () => {
const { http } = await start();

Expand Down
17 changes: 16 additions & 1 deletion workers/collab/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ function isAllowedOrigin(originHeader: string | null, envAllowed?: string): bool
.filter(Boolean)
: [
"https://geolibre.app",
"https://web.geolibre.app",
"https://viewer.geolibre.app",
"https://studio.geolibre.app",
"https://collab.geolibre.app",
"http://localhost",
"http://127.0.0.1",
Expand All @@ -60,7 +63,19 @@ function isAllowedOrigin(originHeader: string | null, envAllowed?: string): bool
try {
const originUrl = new URL(originHeader);
const host = originUrl.hostname;
if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".localhost")) return true;
if (!envAllowed) {
if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".localhost")) return true;
const previewSuffix = ".geolibre-preview.pages.dev";
const previewLabel = host.endsWith(previewSuffix) ? host.slice(0, -previewSuffix.length) : "";
if (
originUrl.protocol === "https:" &&
!originUrl.port &&
previewLabel &&
!previewLabel.includes(".")
) {
return true;
}
}
return allowedList.some((allowed) => {
if (allowed === "*") return true;
try {
Expand Down
Loading