[GH-ISSUE #2015] ProxyAuthService::validateProxyIp relies on $request->ip() (which may honor X-Forwarded-For) rather than raw socket address #1061

Closed
opened 2026-02-26 02:35:07 +03:00 by kerem · 3 comments
Owner

Originally created by @Anduin2017 on GitHub (Jul 11, 2025).
Original GitHub issue: https://github.com/koel/koel/issues/2015

ProxyAuthService::validateProxyIp relies on $request->ip() (which may honor X-Forwarded-For) rather than raw socket address, causing SSO to fail when client IP ≠ proxy IP

github.com/koel/koel@ccbcea8255/app/Services/ProxyAuthService.php (L37)


Description

In ProxyAuthService the call to $request->ip() is used to validate that the incoming request comes from a trusted proxy. However, Laravel/Symfony’s $request->ip() will, by default, trust X-Forwarded-For headers when the client’s IP is in the “trusted proxies” list. This means:

  • If the real client IP (from X-Forwarded-For) falls within one of the allowed CIDRs in PROXY_AUTH_ALLOW_LIST, the request is accepted—even if the proxy’s own IP is not allowed.
  • Conversely, if the real client IP is outside those CIDRs, the request is rejected—even though it actually came through the proxy.

This behavior undermines the whole point of restricting proxy authentication to known reverse-proxy addresses, and can both open security holes (clients spoofing headers to match allowed ranges) and break valid SSO when client IPs differ from proxy IPs.


Steps to Reproduce

  1. Configure Koel with Proxy Authentication enabled in .env:

    PROXY_AUTH_ENABLED=true
    PROXY_AUTH_USER_HEADER=X-Authentik-Username
    PROXY_AUTH_PREFERRED_NAME_HEADER=X-Authentik-Name
    PROXY_AUTH_ALLOW_LIST=10.0.0.0/8,192.168.0.0/16
    
  2. Ensure your reverse proxy (e.g. Caddy/Traefik) is set as a trusted proxy in Laravel’s TrustProxies middleware so that $request->ip() reflects the client IP from X-Forwarded-For.

  3. From a client IP in 192.168.0.0/16, access Koel through the proxy → SSO succeeds (because $request->ip() = client IP in allow list).

  4. From a client IP outside 192.168.0.0/16, access Koel through the same proxy → SSO fails (because $request->ip() = client IP, not proxy’s IP).


Expected Behavior

validateProxyIp() should verify the proxy’s own address (i.e. the TCP source IP) against proxy_auth.allow_list, not the client IP forwarded by headers. That ensures only configured reverse proxies can perform header-based SSO, regardless of end-user address.


Actual Behavior

validateProxyIp() calls IpUtils::checkIp($request->ip(), …), and $request->ip() may resolve to the real client’s IP (from X-Forwarded-For) instead of the proxy’s IP, allowing unauthorized header injection or outright blocking valid SSO sessions.


Possible Solutions

  1. Use raw socket address: Replace $request->ip() with $request->server->get('REMOTE_ADDR') for proxy validation.
  2. Make header vs. socket configurable: Add a config option proxy_auth.use_forwarded_ip (default false) to toggle whether to trust forwarded IPs.
  3. Enhance logging: Log both the REMOTE_ADDR and the “computed” client IP to help operators debug mis-matches.

Environment

version: '3.9'

services:
  authentik_proxy:
    image: hub.aiursoft.cn/ghcr.io/goauthentik/proxy:2025.6
    environment:
      AUTHENTIK_HOST: "https://auth.aiursoft.cn/"
      AUTHENTIK_INSECURE: "false"
      AUTHENTIK_TOKEN: "{{KOEL_OUTPOST_TOKEN}}"
    networks:
      - proxy_app

  koel:
    image: hub.aiursoft.cn/phanan/koel
    depends_on:
      - database
    environment:
      - DB_CONNECTION=mysql
      - DB_HOST=koel-db
      - DB_USERNAME=koel
      - DB_PASSWORD=<koel_password>
      - DB_DATABASE=koel
      - MEDIA_PATH=/music
      - FORCE_HTTPS=true
    volumes:
      - music:/music
      - covers:/var/www/html/public/img/covers
      - search_index:/var/www/html/storage/search-indexes
      - /swarm-vol/koel/config:/var/www/html/.env # This is a single file, not a directory
    networks:
      - internal
      - proxy_app
    deploy:
      labels:
        swarmpit.service.deployment.autoredeploy: 'true'

  koel-db:
    image: hub.aiursoft.cn/mysql
    volumes:
      - db:/var/lib/mysql
    environment:
      - MYSQL_RANDOM_ROOT_PASSWORD=true
      - MYSQL_DATABASE=koel
      - MYSQL_USER=koel
      - MYSQL_PASSWORD=<koel_password>
    networks:
      - internal
    deploy:
      labels:
        swarmpit.service.deployment.autoredeploy: 'true'

networks:
  internal:
    driver: overlay
  proxy_app:
    external: true

volumes:
  db:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /swarm-vol/koel/db
  music:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /swarm-vol/koel/music
  covers:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /swarm-vol/koel/covers
  search_index:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /swarm-vol/koel/search_index

# Now we have koel_koel:80
# And we have koel_authentik_proxy:9000

# Protected by Authentik
musics.aiursoft.cn {
    log
    import hsts
    import rate_limit
    header -content-security-policy
    header -x-frame-options
    encode br gzip

    route {
        reverse_proxy /manifest.json http://koel_koel:80

        reverse_proxy /outpost.goauthentik.io/* http://koel_authentik_proxy:9000

        forward_auth http://koel_authentik_proxy:9000 {
            uri     /outpost.goauthentik.io/auth/caddy
            copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Entitlements X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version
        }

        reverse_proxy http://koel_koel:80
    }
}

PROXY_AUTH_ENABLED=true
PROXY_AUTH_USER_HEADER=X-Authentik-Username
PROXY_AUTH_PREFERRED_NAME_HEADER=X-Authentik-Name
PROXY_AUTH_ALLOW_LIST=10.0.0.0/8,192.168.0.0/16,172.16.0.0/12

Originally created by @Anduin2017 on GitHub (Jul 11, 2025). Original GitHub issue: https://github.com/koel/koel/issues/2015 `ProxyAuthService::validateProxyIp` relies on `$request->ip()` (which may honor `X-Forwarded-For`) rather than raw socket address, causing SSO to fail when client IP ≠ proxy IP https://github.com/koel/koel/blob/ccbcea8255c20c99983b82a9ab3a4808e6f2dc65/app/Services/ProxyAuthService.php#L37 --- ## Description In **ProxyAuthService** the call to `$request->ip()` is used to validate that the incoming request comes from a trusted proxy. However, Laravel/Symfony’s `$request->ip()` will, by default, trust **`X-Forwarded-For`** headers when the client’s IP is in the “trusted proxies” list. This means: * If the real client IP (from `X-Forwarded-For`) falls within one of the allowed CIDRs in `PROXY_AUTH_ALLOW_LIST`, the request is accepted—even if the proxy’s own IP is not allowed. * Conversely, if the real client IP is outside those CIDRs, the request is rejected—even though it actually came through the proxy. This behavior undermines the whole point of restricting proxy authentication to known reverse-proxy addresses, and can both open security holes (clients spoofing headers to match allowed ranges) and break valid SSO when client IPs differ from proxy IPs. --- ## Steps to Reproduce 1. Configure Koel with Proxy Authentication enabled in `.env`: ```dotenv PROXY_AUTH_ENABLED=true PROXY_AUTH_USER_HEADER=X-Authentik-Username PROXY_AUTH_PREFERRED_NAME_HEADER=X-Authentik-Name PROXY_AUTH_ALLOW_LIST=10.0.0.0/8,192.168.0.0/16 ``` 2. Ensure your reverse proxy (e.g. Caddy/Traefik) is set as a **trusted proxy** in Laravel’s `TrustProxies` middleware so that `$request->ip()` reflects the client IP from `X-Forwarded-For`. 3. From a client IP in `192.168.0.0/16`, access Koel through the proxy → SSO succeeds (because `$request->ip()` = client IP in allow list). 4. From a client IP **outside** `192.168.0.0/16`, access Koel through the same proxy → SSO fails (because `$request->ip()` = client IP, not proxy’s IP). --- ## Expected Behavior `validateProxyIp()` should verify **the proxy’s own address** (i.e. the TCP source IP) against `proxy_auth.allow_list`, **not** the client IP forwarded by headers. That ensures only configured reverse proxies can perform header-based SSO, regardless of end-user address. --- ## Actual Behavior `validateProxyIp()` calls `IpUtils::checkIp($request->ip(), …)`, and `$request->ip()` may resolve to the real client’s IP (from `X-Forwarded-For`) instead of the proxy’s IP, allowing unauthorized header injection or outright blocking valid SSO sessions. --- ## Possible Solutions 1. **Use raw socket address**: Replace `$request->ip()` with `$request->server->get('REMOTE_ADDR')` for proxy validation. 2. **Make header vs. socket configurable**: Add a config option `proxy_auth.use_forwarded_ip` (default `false`) to toggle whether to trust forwarded IPs. 3. **Enhance logging**: Log both the `REMOTE_ADDR` and the “computed” client IP to help operators debug mis-matches. --- ## Environment ```yaml version: '3.9' services: authentik_proxy: image: hub.aiursoft.cn/ghcr.io/goauthentik/proxy:2025.6 environment: AUTHENTIK_HOST: "https://auth.aiursoft.cn/" AUTHENTIK_INSECURE: "false" AUTHENTIK_TOKEN: "{{KOEL_OUTPOST_TOKEN}}" networks: - proxy_app koel: image: hub.aiursoft.cn/phanan/koel depends_on: - database environment: - DB_CONNECTION=mysql - DB_HOST=koel-db - DB_USERNAME=koel - DB_PASSWORD=<koel_password> - DB_DATABASE=koel - MEDIA_PATH=/music - FORCE_HTTPS=true volumes: - music:/music - covers:/var/www/html/public/img/covers - search_index:/var/www/html/storage/search-indexes - /swarm-vol/koel/config:/var/www/html/.env # This is a single file, not a directory networks: - internal - proxy_app deploy: labels: swarmpit.service.deployment.autoredeploy: 'true' koel-db: image: hub.aiursoft.cn/mysql volumes: - db:/var/lib/mysql environment: - MYSQL_RANDOM_ROOT_PASSWORD=true - MYSQL_DATABASE=koel - MYSQL_USER=koel - MYSQL_PASSWORD=<koel_password> networks: - internal deploy: labels: swarmpit.service.deployment.autoredeploy: 'true' networks: internal: driver: overlay proxy_app: external: true volumes: db: driver: local driver_opts: type: none o: bind device: /swarm-vol/koel/db music: driver: local driver_opts: type: none o: bind device: /swarm-vol/koel/music covers: driver: local driver_opts: type: none o: bind device: /swarm-vol/koel/covers search_index: driver: local driver_opts: type: none o: bind device: /swarm-vol/koel/search_index ``` ```conf # Now we have koel_koel:80 # And we have koel_authentik_proxy:9000 # Protected by Authentik musics.aiursoft.cn { log import hsts import rate_limit header -content-security-policy header -x-frame-options encode br gzip route { reverse_proxy /manifest.json http://koel_koel:80 reverse_proxy /outpost.goauthentik.io/* http://koel_authentik_proxy:9000 forward_auth http://koel_authentik_proxy:9000 { uri /outpost.goauthentik.io/auth/caddy copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Entitlements X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version } reverse_proxy http://koel_koel:80 } } ``` ```php PROXY_AUTH_ENABLED=true PROXY_AUTH_USER_HEADER=X-Authentik-Username PROXY_AUTH_PREFERRED_NAME_HEADER=X-Authentik-Name PROXY_AUTH_ALLOW_LIST=10.0.0.0/8,192.168.0.0/16,172.16.0.0/12 ```
kerem closed this issue 2026-02-26 02:35:07 +03:00
Author
Owner

@Anduin2017 commented on GitHub (Jul 11, 2025):

Possible fix? (I'm not a PHP expert, not tested):

    $proxyIp = $request->server->get('REMOTE_ADDR');
    return IpUtils::checkIp(
        $proxyIp,
        config('koel.proxy_auth.allow_list')
    );
<!-- gh-comment-id:3061517397 --> @Anduin2017 commented on GitHub (Jul 11, 2025): Possible fix? (I'm not a PHP expert, not tested): ```php $proxyIp = $request->server->get('REMOTE_ADDR'); return IpUtils::checkIp( $proxyIp, config('koel.proxy_auth.allow_list') ); ```
Author
Owner

@phanan commented on GitHub (Jul 13, 2025):

Good point, and thanks for the PR. TBH I'm not a network expert (which is the major reason why this feature is in Beta). I'll take a further look :)

<!-- gh-comment-id:3066727328 --> @phanan commented on GitHub (Jul 13, 2025): Good point, and thanks for the PR. TBH I'm not a network expert (which is the major reason why this feature is in Beta). I'll take a further look :)
Author
Owner

@phanan commented on GitHub (Jul 13, 2025):

Closesd by the PR.

<!-- gh-comment-id:3066744417 --> @phanan commented on GitHub (Jul 13, 2025): Closesd by the PR.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/koel-koel#1061
No description provided.