makeplane / makeplane/plane

[bug]: live server CORS falls back to origin [""] instead of false, short-circuiting every OPTIONS request

Open
#9,843 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
59.6k
Forks
5.8k
Avg merge
1d 22h
Merged PRs (30d)
49

Description

Is there an existing issue for this?
  • I have searched the existing issues
Current behavior

When CORS_ALLOWED_ORIGINS is unset, the live server is meant to deny all cross-origin
requests via origin: false. That branch is unreachable, and what it falls back to
instead changes how the server answers every OPTIONS request.

apps/live/src/server.ts:71-81:

const allowedOrigins = env.CORS_ALLOWED_ORIGINS.split(",").map((s) => s.trim());
this.app.use(
  cors({
    origin: allowedOrigins.length > 0 ? allowedOrigins : false,
    credentials: true,
    ...

CORS_ALLOWED_ORIGINS defaults to "" at apps/live/src/env.ts:19, and "".split(",")
returns [""], not []. Length is 1, so allowedOrigins.length > 0 is always true and
the : false branch can never run. origin is set to [""].

The two are not equivalent. In cors, a falsy origin means the middleware calls
next() without touching the response, so OPTIONS reaches your router. A truthy array
means it runs the full preflight path and answers OPTIONS itself with
optionsSuccessStatus, which defaults to 204.

Measured on cors@2.8.6 / express@5.2.1 with the exact options from server.ts:

origin:false  OPTIONS -> 200  (no cors headers)
origin:false  POST    -> 200  (no cors headers)
origin:[""]   OPTIONS -> 204  access-control-allow-credentials: true |
                              access-control-allow-headers: Content-Type,Authorization,x-api-key |
                              access-control-allow-methods: GET,POST,PUT,DELETE,OPTIONS |
                              vary: Origin
origin:[""]   POST    -> 200  access-control-allow-credentials: true | vary: Origin

So today, on a default install:

  • every OPTIONS to any /live/* route is swallowed by the CORS middleware and answered
    204, instead of reaching the router or the 404 handler in setupNotFoundHandler
  • preflights advertise Access-Control-Allow-Credentials: true along with the allowed
    methods and headers, for an origin list that allows nothing
  • every response carries Vary: Origin and Access-Control-Allow-Credentials: true

To be clear about severity: no Access-Control-Allow-Origin is ever emitted, so browsers
still block the cross-origin read. This is not a vulnerability. It is the server
advertising a CORS posture it does not have, and answering a method it did not intend to
handle.

There is a second half to this. CORS_ALLOWED_ORIGINS never reaches the live container in
a compose deploy. In deployments/cli/community/docker-compose.yml it is defined at line
52 inside the x-app-env anchor, while the live service takes
<<: [*live-env, *redis-env], and x-live-env at line 45 holds only API_BASE_URL and
LIVE_SERVER_SECRET_KEY. apps/live/.env.example does not list it either. So the knob
cannot currently be set for the live server at all, which is why the empty-string path is
the one everybody is on.

Worth knowing while fixing it: cors only honours "*" as a wildcard when origin is a
bare string. Inside an array it is compared with origin === allowedOrigin, so once the
variable is plumbed through, CORS_ALLOWED_ORIGINS=* would deny everything rather than
allow it.

The Django side already gets all of this right, and comments the reason.
apps/api/plane/settings/common.py:182-186:

cors_origins_raw = os.environ.get("CORS_ALLOWED_ORIGINS", "")
# filter out empty strings
cors_allowed_origins = [origin.strip() for origin in cors_origins_raw.split(",") if origin.strip()]
if cors_allowed_origins:
    CORS_ALLOWED_ORIGINS = cors_allowed_origins
else:
    CORS_ALLOW_ALL_ORIGINS = True
Steps to reproduce

No Plane instance needed, the parse is the whole bug:

node -e 'console.log(JSON.stringify("".split(",")))'   # => [""]  , length 1, not []

To see the behavioural difference, stand up two Express apps with the options from
server.ts, one with origin: false and one with origin: [""], and send an OPTIONS
with an Origin header to each. The first returns 200 from the route with no CORS
headers; the second returns 204 from the CORS middleware with credentials, methods and
headers advertised.

On a real deployment:

  1. Start a community compose deployment from deployments/cli/community with
    CORS_ALLOWED_ORIGINS set in variables.env.
  2. docker compose exec live env | grep CORS — the variable is absent.
  3. curl -i -X OPTIONS -H 'Origin: https://example.com' https://<host>/live/health
    204 with Access-Control-Allow-Credentials: true, rather than the route's response.
Environment

Deploy preview

Edition

Community

Version

v1.4.2, also present on preview as of 15 Sep 2026


Suggested fix

Mirror the filter the Django settings already use, so the intended false is reachable:

const allowedOrigins = env.CORS_ALLOWED_ORIGINS.split(",")
  .map((s) => s.trim())
  .filter(Boolean);

origin: allowedOrigins.length > 0 ? allowedOrigins : false,

and plumb the variable through so the knob works at all: add
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS} to x-live-env in
deployments/cli/community/docker-compose.yml, and add it to apps/live/.env.example.

Happy to open a PR for this if it is wanted.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the CORS setup in apps/live/src/server.ts and the default in apps/live/src/env.ts, then inspect x-live-env and the live service in deployments/cli/community/docker-compose.yml plus apps/live/.env.example. Reproduce the empty-string parsing and OPTIONS behavior described in the issue. Done means empty origins reach the intended disabled path and CORS_ALLOWED_ORIGINS is available inside the live container.

Written by the indexing model from the issue text.

Assessment

Tech stack
docker-compose, express, typescript
Domain
backend, devops, infrastructure
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.