Contribution guidelines
Checklist prior issue creation
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:
- 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.
- Instead enter a bare hostname, e.g.
localhost, and save.
- Observed: saves successfully.
- 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.
Logs:
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:
Logs of iptables -L -vn:
Logs of ip6tables -L -vn:
Logs of iptables -L -vn -t nat:
Logs of ip6tables -L -vn -t nat:
DNS check:
Contribution guidelines
Checklist prior issue creation
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:
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:FILTER_VALIDATE_DOMAIN/FILTER_FLAG_HOSTNAMEonly accepts strings likeexample.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
Originheader is always the fullscheme://host[:port], never a bare hostname. The matching code later on does an exact string comparison against that header:Since
$allowed_originscan only ever contain bare hostnames, and$_SERVER['HTTP_ORIGIN']is always a full origin, thisin_arraycheck 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:
http://localhost:3000, then click Save.cors_invalid_origin) — a full origin can never be saved at all.localhost, and save.http://localhost:3000, make afetch()request to the mailcow API (/api/v1/get/...) with an appropriate auth header.Access-Control-Allow-Origindoes not match'localhost'(or the mismatch error otherwise names whichever entry is first in the configured list, regardless of which entry should logically apply).localhost:3000was intended to be allowlisted.Logs:
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:
Logs of iptables -L -vn:
Logs of ip6tables -L -vn:
Logs of iptables -L -vn -t nat:
Logs of ip6tables -L -vn -t nat:
DNS check: