go package report

Is github.com/tinyauthapp/tinyauth safe?

2 known vulnerabilities, worst severity HIGH.

// reach

0 direct dependencies

none carry a known advisory

    0 packages depend on it

    an advisory here reaches each of them

      Create a free accountfor every dependency path, dependent and what to upgrade
      // ai model usage

      Tracked for PyPI packages. HuggingFace models declare Python dependencies, so go packages are not covered.


      cvss
      0.0
      high

      severity out of 10

      epss
      not scored

      chance of exploitation in 30 days

      xyz score
      not scored

      CyberXYZ composite out of 10

      fig. 01 — GHSA-328g-jx67-v94g, the advisory selected below

      // 2 advisories

      GHSA-328g-jx67-v94g

      HIGHCVE-2026-77560
      // 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 (commit 479f1657812b); root cause also present on main HEAD
      • Component: internal/service/accesscontrolsservice.go (lookupStaticACLs / GetAccessControls), internal/service/dockerservice.go (GetLabels), internal/controller/proxycontroller.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/ 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/accesscontrolsservice.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/dockerservice.go:

      func (docker *DockerService) GetLabels(appDomain string) (config.App, error) {
      	if !docker.isConnected {
      		return config.App{}, nil            //  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/proxycontroller.go and internal/service/authservice.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.

      // cvss v3.1 vector

      CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N

      Attack vector
      Network
      Attack complexity
      Low
      Privileges required
      Low
      User interaction
      None
      Scope
      Unchanged
      Confidentiality
      High
      Integrity
      High
      Availability
      None

      Checked 2026-09-22 at 22:30 UTC. The most recent advisory here was published 2026-09-22. Updated continuously from NVD, GHSA, OSV and CNA feeds.

      Think a verdict here is wrong? Tell us — we respond within 2 business days.
      Is github.com/tinyauthapp/tinyauth safe? go package security report | CyberXYZ