Skip to content

CORS "Allowed Origins" only accepts bare hostnames, so it can never match a real browser Origin header #7332

Description

@fallmo

Contribution guidelines

Checklist prior issue creation

  • I understand that failure to follow below instructions may cause this issue to be closed.
  • I understand that vague, incomplete or inaccurate information may cause this issue to be closed.
  • I understand that this form is intended solely for reporting software bugs and not for support-related inquiries.
  • I understand that all responses are voluntary and community-driven, and do not constitute commercial support.
  • I confirm that I have reviewed previous issues to ensure this matter has not already been addressed.
  • I confirm that my environment meets all prerequisite requirements as specified in the official documentation.

Description

The API CORS settings (Configuration → Administrative Settings → CORS) let an admin configure an "Allowed Origins" list, intended to permit browser JavaScript on other origins to call the mailcow API cross-origin — as this project's own 2023-07 release announcement describes it:

"Other origins" ... may include having different domain, port or scheme (HTTP or HTTPS).

That's the correct definition of an origin. However, the actual validation in functions.inc.php, cors(), case "edit" rejects anything that isn't a bare hostname:

// functions.inc.php:2289
if (!filter_var($origin, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) && $origin != '*') {

FILTER_VALIDATE_DOMAIN / FILTER_FLAG_HOSTNAME only accepts strings like example.com — a scheme (http://) or port (:3000) fails validation outright.

This breaks the feature at a fundamental level: per the Fetch/CORS spec, a browser's Origin header is always the full scheme://host[:port], never a bare hostname. The matching code later on does an exact string comparison against that header:

// functions.inc.php:2354-2359
$allowed_origins = explode(', ', $cors_settings['allowed_origins']);
$cors_settings['allowed_origins'] = $allowed_origins[0];
if (in_array('*', $allowed_origins)){
  $cors_settings['allowed_origins'] = '*';
} else if (array_key_exists('HTTP_ORIGIN', $_SERVER) && in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins)) {
  $cors_settings['allowed_origins'] = $_SERVER['HTTP_ORIGIN'];
}

Since $allowed_origins can only ever contain bare hostnames, and $_SERVER['HTTP_ORIGIN'] is always a full origin, this in_array check can never succeed for any real cross-origin browser request. The code silently falls back to reflecting $allowed_origins[0] (the first configured entry) regardless — so the browser always reports a mismatch against whatever the first list entry happens to be, no matter how many entries are configured or which one should theoretically apply.

Impact: as shipped, this setting cannot actually allow any specific origin to make a CORS-checked browser request to the API — it only works at all via the * wildcard, which defeats the purpose of an allowlist.

Steps to reproduce:

  1. Go to the CORS settings page and enter a real origin including scheme and port in "Allowed Origins", e.g. http://localhost:3000, then click Save.
    • Observed: save fails with "Invalid CORS origin" (cors_invalid_origin) — a full origin can never be saved at all.
  2. Instead enter a bare hostname, e.g. localhost, and save.
    • Observed: saves successfully.
  3. From a page served at http://localhost:3000, make a fetch() request to the mailcow API (/api/v1/get/...) with an appropriate auth header.
    • Observed: browser blocks the request — Access-Control-Allow-Origin does not match 'localhost' (or the mismatch error otherwise names whichever entry is first in the configured list, regardless of which entry should logically apply).
    • Expected: the request should succeed, since localhost:3000 was intended to be allowlisted.
Image

Logs:

N/A

Which branch are you using?

master (stable)

Which architecture are you using?

x86_64

Operating System:

N/A

Server/VM specifications:

N/A

Is Apparmor, SELinux or similar active?

N/A

Virtualization technology:

N/A

Docker version:

N/A

docker-compose version or docker compose version:

N/A

mailcow version:

2026-05c

Reverse proxy:

none

Logs of git diff:

N/A

Logs of iptables -L -vn:

N/A

Logs of ip6tables -L -vn:

N/A

Logs of iptables -L -vn -t nat:

N/A

Logs of ip6tables -L -vn -t nat:

N/A

DNS check:

N/A

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions