GHSA-m687-p538-r5hp: Vikunja Permissive Cross-domain Security Policy Trusts Vulnerability

HIGH
CWE-942
Published
Advisory Summary

GHSA-m687-p538-r5hp is a HIGH permissive cross-domain security policy trusts vulnerability (CWE-942: Permissive Cross-domain Security Policy with Untrusted Domains) affecting Vikunja libraries in the Go ecosystem. The issue involves permissive cross-domain security policy trusts every localhost origin which should not be trusted. Remediation is available in the latest vendor maintenance releases.

GHSA-m687-p538-r5hp at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-m687-p538-r5hp
Severity HIGH
CVSS v3.1 Base Score N/A
CWE Classification CWE-942: Permissive Cross-domain Security Policy with Untrusted Domains
Affected Product Vikunja
Package Ecosystem Go
Published Date
Last Updated

How to Fix GHSA-m687-p538-r5hp

To remediate this vulnerability, upgrade all affected Vikunja dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

code.vikunja.io/api fixed: Latest secure release
Upgrade code.vikunja.io/api to Latest secure release
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for code.vikunja.io/api

How to Check if You Are Affected

To determine whether your application is affected by GHSA-m687-p538-r5hp, inspect your project dependencies for Vikunja:

inspect dependencies for code.vikunja.io/api

Analyze the terminal output to locate any version of Vikunja in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than the patched release), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-942: Permissive Cross-domain Security Policy with Untrusted Domains, where the product uses a web-client protection mechanism such as a content security policy (csp) or cross-domain policy file, but the policy includes untrusted domains with which the web client is allowed to communicate. In Vikunja, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary Vikunja ships with `cors.origins` defaulting to `http://127.0.0.1:*` and `http://localhost:*`, and sends `Access-Control-Allow-Credentials: true`, so a page served from any port on the user's own machine may make credentialed cross-origin requests to the API and read the responses. The mandatory `service.publicurl` is appended to that default list rather than replacing it, so a fully configured production deployment still trusts every localhost origin. Combined with the token refresh endpoint, which authenticates with the `vikunja_refresh_token` cookie and returns a bearer JWT in the response body, one `fetch()` from such a page yields a working access token for whoever is logged in, and from there the whole account. ## Vulnerability Details `cors.enable` defaults to true and `cors.origins` defaults to the two localhost wildcards: https://github.com/go-vikunja/vikunja/blob/a881ac39eecd07575a5aede8e74727c28d5fd578/pkg/config/config.go#L495-L497 The middleware is registered with `AllowCredentials: true` and an `UnsafeAllowOriginFunc` that implements the port wildcard through `matchCORSOrigin`, since Echo v5 will not accept a wildcard port itself: https://github.com/go-vikunja/vikunja/blob/a881ac39eecd07575a5aede8e74727c28d5fd578/pkg/routes/routes.go#L263-L281 The detail that turns a development convenience into a production exposure is the last line of configuration handling. Enabling CORS without a public URL is a fatal error, so every deployment sets one, and that value is *appended* to the origin list rather than substituted for it: https://github.com/go-vikunja/vikunja/blob/a881ac39eecd07575a5aede8e74727c28d5fd578/pkg/config/config.go#L828-L830 An operator who configures nothing but their own hostname therefore ends up allowing three origins with credentials: their site, and any port on `localhost` and `127.0.0.1`. There is no supported configuration in which the localhost entries are quietly dropped, and nothing in the logs distinguishes the intended origin from the inherited ones. The escalation path is the refresh endpoint. It is deliberately unauthenticated because it authenticates with the refresh cookie instead of a bearer token, and it returns the new access token in the JSON body: https://github.com/go-vikunja/vikunja/blob/a881ac39eecd07575a5aede8e74727c28d5fd578/pkg/routes/api/v1/login.go#L129-L150 The cookie is `HttpOnly`, but that offers no protection here, because the attacking page never reads the cookie. It issues a credentialed request and the browser attaches the cookie itself. `SetRefreshTokenCookie` marks the cookie `SameSite=None` whenever the public URL is `https`, precisely so the cookie survives split-origin deployments, which is also what allows a cross-site send from a localhost page: https://github.com/go-vikunja/vikunja/blob/a881ac39eecd07575a5aede8e74727c28d5fd578/pkg/modules/auth/auth.go#L84-L104 So a single `fetch('https://vikunja.example.com/api/v1/user/token/refresh', {method: 'POST', credentials: 'include'})` from any page on any localhost port returns a bearer token for the logged-in user, and the CORS response headers permit the page to read it. The token carries the user's full API authority. ## Proof of Concept Three files, built together in one directory and run with: [poc.zip](https://github.com/user-attachments/files/31917334/poc.zip) ``` docker build -f Dockerfile -t poc . && docker run --rm poc ``` - `Dockerfile` builds Vikunja from commit `a881ac39eecd07575a5aede8e74727c28d5fd578` and bakes the reproducer in. - `poc.sh` is the entrypoint. It starts the application on sqlite with `service.publicurl` as the only setting configured, waits for the health check, and runs the exploit. - `exploit.py` registers a user, logs them in, then acts as a page on `http://localhost:31337`: it reads the refresh cookie attributes, sends the CORS preflight, makes the credentialed refresh request, and asks the API who the returned token belongs to. Nothing is stubbed and no internal function is called; everything after startup is ordinary HTTP against the running application. In the output `[+]` lines are measured checks and `[*]` lines are commentary, so only the checks and the final verdict are evidence. A run against `a881ac39eecd07575a5aede8e74727c28d5fd578` prints: ``` [*] Vikunja built from commit a881ac39ee, sqlite, service.publicurl=https://vikunja.example.com. [*] service.publicurl is the only setting configured. cors.enable and cors.origins keep their defaults. [+] The victim logs in and the refresh cookie is issued SameSite=None; Secure, so it is sent cross-site. [+] A page on http://localhost:31337 is allowed to send credentials: Access-Control-Allow-Origin: http://localhost:31337, Access-Control-Allow-Credentials: true [+] That page reads the response of a credentialed refresh, which carries a bearer token: eyJhbGciOiJIUzI1NiIsInR5cCI6... [+] The token authenticates as: 'victim' EXPLOIT SUCCESSFUL ``` ## Suggested Fix The fix is for `service.publicurl` to replace the localhost defaults rather than extend them, so that configuring a deployment narrows the allowed origins instead of widening them. If the localhost entries are retained deliberately for desktop clients, they should not be combined with `AllowCredentials: true`, since it is the combination that allows a third-party origin to read authenticated responses. ## Attribution This vulnerability was found using Google's security automation tooling, abd triaged manually with manual report writing by Ada Logics. Please credit Google and Ada Logics in any advisories.

GHSA-m687-p538-r5hp Affected Vikunja Versions

1 component(s)
Product / Package Ecosystem Affected Versions Fixed Version
code.vikunja.io/api
Go
≥ 2.2.0
≤ 2.6.0

References

Frequently Asked Questions

What is GHSA-m687-p538-r5hp?
GHSA-m687-p538-r5hp is a security vulnerability in Vikunja involving Permissive Cross-domain Security Policy trusts every localhost origin which should not be trusted under CWE-942: Permissive Cross-domain Security Policy with Untrusted Domains.
Which Vikunja versions are affected?
Affected versions include releases prior to current release patches across affected packages: code.vikunja.io/api.
What version fixes GHSA-m687-p538-r5hp?
Upgrading to current release patches (or the latest patched release for your respective branch) resolves the vulnerability.
Is code.vikunja.io/api affected?
Yes, code.vikunja.io/api is affected in releases prior to Latest secure release. Upgrade your dependency to Latest secure release or later.
What CWE is associated with GHSA-m687-p538-r5hp?
GHSA-m687-p538-r5hp is classified under CWE-942: Permissive Cross-domain Security Policy with Untrusted Domains (The product uses a web-client protection mechanism such as a Content Security Policy (CSP) or cross-domain policy file, but the policy includes untrusted domains with which the web client is allowed to communicate.).
How can I check Go dependencies for GHSA-m687-p538-r5hp?
Run the dependency tree command: 'inspect dependencies for code.vikunja.io/api'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-m687-p538-r5hp",
  "modified" : "2026-10-09T20:57:50Z",
  "published" : "2026-10-09T20:57:50Z",
  "aliases" : [ ],
  "summary" : "Vikunja: Permissive Cross-domain Security Policy trusts every localhost origin which should not be trusted",
  "details" : "## Summary\n\nVikunja ships with `cors.origins` defaulting to `http://127.0.0.1:*` and `http://localhost:*`, and sends `Access-Control-Allow-Credentials: true`, so a page served from any port on the user's own machine may make credentialed cross-origin requests to the API and read the responses. The mandatory `service.publicurl` is appended to that default list rather than replacing it, so a fully configured production deployment still trusts every localhost origin. Combined with the token refresh endpoint, which authenticates with the `vikunja_refresh_token` cookie and returns a bearer JWT in the response body, one `fetch()` from such a page yields a working access token for whoever is logged in, and from there the whole account.\n\n## Vulnerability Details\n\n`cors.enable` defaults to true and `cors.origins` defaults to the two localhost wildcards:\n\nhttps://github.com/go-vikunja/vikunja/blob/a881ac39eecd07575a5aede8e74727c28d5fd578/pkg/config/config.go#L495-L497\n\nThe middleware is registered with `AllowCredentials: true` and an `UnsafeAllowOriginFunc` that implements the port wildcard through `matchCORSOrigin`, since Echo v5 will not accept a wildcard port itself:\n\nhttps://github.com/go-vikunja/vikunja/blob/a881ac39eecd07575a5aede8e74727c28d5fd578/pkg/routes/routes.go#L263-L281\n\nThe detail that turns a development convenience into a production exposure is the last line of configuration handling. Enabling CORS without a public URL is a fatal error, so every deployment sets one, and that value is *appended* to the origin list rather than substituted for it:\n\nhttps://github.com/go-vikunja/vikunja/blob/a881ac39eecd07575a5aede8e74727c28d5fd578/pkg/config/config.go#L828-L830\n\nAn operator who configures nothing but their own hostname therefore ends up allowing three origins with credentials: their site, and any port on `localhost` and `127.0.0.1`. There is no supported configuration in which the localhost entries are quietly dropped, and nothing in the logs distinguishes the intended origin from the inherited ones.\n\nThe escalation path is the refresh endpoint. It is deliberately unauthenticated because it authenticates with the refresh cookie instead of a bearer token, and it returns the new access token in the JSON body:\n\nhttps://github.com/go-vikunja/vikunja/blob/a881ac39eecd07575a5aede8e74727c28d5fd578/pkg/routes/api/v1/login.go#L129-L150\n\nThe cookie is `HttpOnly`, but that offers no protection here, because the attacking page never reads the cookie. It issues a credentialed request and the browser attaches the cookie itself. `SetRefreshTokenCookie` marks the cookie `SameSite=None` whenever the public URL is `https`, precisely so the cookie survives split-origin deployments, which is also what allows a cross-site send from a localhost page:\n\nhttps://github.com/go-vikunja/vikunja/blob/a881ac39eecd07575a5aede8e74727c28d5fd578/pkg/modules/auth/auth.go#L84-L104\n\nSo a single `fetch('https://vikunja.example.com/api/v1/user/token/refresh', {method: 'POST', credentials: 'include'})` from any page on any localhost port returns a bearer token for the logged-in user, and the CORS response headers permit the page to read it. The token carries the user's full API authority.\n\n## Proof of Concept\n\nThree files, built together in one directory and run with:\n\n[poc.zip](https://github.com/user-attachments/files/31917334/poc.zip)\n\n```\ndocker build -f Dockerfile -t poc . && docker run --rm poc\n```\n\n- `Dockerfile` builds Vikunja from commit `a881ac39eecd07575a5aede8e74727c28d5fd578` and bakes the reproducer in.\n- `poc.sh` is the entrypoint. It starts the application on sqlite with `service.publicurl` as the only setting configured, waits for the health check, and runs the exploit.\n- `exploit.py` registers a user, logs them in, then acts as a page on `http://localhost:31337`: it reads the refresh cookie attributes, sends the CORS preflight, makes the credentialed refresh request, and asks the API who the returned token belongs to.\n\nNothing is stubbed and no internal function is called; everything after startup is ordinary HTTP against the running application. In the output `[+]` lines are measured checks and `[*]` lines are commentary, so only the checks and the final verdict are evidence.\n\nA run against `a881ac39eecd07575a5aede8e74727c28d5fd578` prints:\n\n```\n[*] Vikunja built from commit a881ac39ee, sqlite, service.publicurl=https://vikunja.example.com.\n[*] service.publicurl is the only setting configured. cors.enable and cors.origins keep their defaults.\n\n[+] The victim logs in and the refresh cookie is issued SameSite=None; Secure, so it is sent cross-site.\n[+] A page on http://localhost:31337 is allowed to send credentials: Access-Control-Allow-Origin: http://localhost:31337, Access-Control-Allow-Credentials: true\n[+] That page reads the response of a credentialed refresh, which carries a bearer token: eyJhbGciOiJIUzI1NiIsInR5cCI6...\n[+] The token authenticates as: 'victim'\n\nEXPLOIT SUCCESSFUL\n```\n\n## Suggested Fix\n\nThe fix is for `service.publicurl` to replace the localhost defaults rather than extend them, so that configuring a deployment narrows the allowed origins instead of widening them. If the localhost entries are retained deliberately for desktop clients, they should not be combined with `AllowCredentials: true`, since it is the combination that allows a third-party origin to read authenticated responses.\n\n## Attribution\n\nThis vulnerability was found using Google's security automation tooling, abd triaged manually with manual report writing by Ada Logics. Please credit Google and Ada Logics in any advisories.",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "code.vikunja.io/api"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "2.2.0"
      }, {
        "last_affected" : "2.6.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/go-vikunja/vikunja/security/advisories/GHSA-m687-p538-r5hp"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/go-vikunja/vikunja"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-942" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-10-09T20:57:50Z",
    "nvd_published_at" : null
  }
}