bunkerity / bunkerweb

🛡️ Open-source and next-generation Web Application Firewall (WAF)
https://www.bunkerweb.io
GNU Affero General Public License v3.0
6.68k stars 380 forks source link

[BUG] When `Host` is unknown, BunkerWeb will serve the first entry in `SERVER_NAME` even with `DISABLE_DEFAULT_SERVER` set to `yes` #402

Closed axeleroy closed 1 year ago

axeleroy commented 1 year ago

Description To ease the deployment of my self-hosted services, I use a wildcard domain (ie. *.selfhosting.domain.tld) that points to BunkerWeb (so I don't have to log into my registar's console to add a domain for each new service).

I recently realized that if the Host header requests a domain not in this list BunkerWeb will happily serve the first site in SERVER_NAME even though I set DISABLE_DEFAULT_SERVER to yes.

How to reproduce I'm using the Docker integration with Docker Compose (some details were omitted):

version: '3'

services:
  bunkerweb:
    image: bunkerity/bunkerweb:1.4.5
    container_name: bunkerweb
    ports:
      - 8080:8080
      - 8443:8443
    volumes:
      - bunkerweb-data:/data
    environment:
      - DISABLE_DEFAULT_SERVER=yes
      - USE_REAL_IP=yes
      - REAL_IP_FROM=192.168.0.0/24 172.18.0.0/16
      - REAL_IP_HEADER=X-Forwarded-For
      - MULTISITE=yes
      # In real world, SERVER_NAME has many more entries, I wanted to keep the example short and simple
      - SERVER_NAME=service-a.domain.tld service-b.selfhosting.domain.tld
      - AUTO_LETS_ENCRYPT=yes
      - EMAIL_LETS_ENCRYPT=me@domain.tld
      - USE_REVERSE_PROXY=yes
      - SERVE_FILES=no
      - REVERSE_PROXY_URL=/
      - ALLOWED_METHODS=GET|POST|PUT|PATCH|DELETE|HEAD
      - service-a.domain.tld_REVERSE_PROXY_HOST=http://servicea
      - service-b.domain.tld_REVERSE_PROXY_HOST=http://serviceb
   networks:
      - bunkerweb-net
    restart: unless-stopped

volumes:
  bunkerweb-data: {}

networks:
  bunkerweb-net:
    name: bunkerweb-net

Now, if I use curl to GET with Host as foo.selfhosting.domain.tld or even google.com I get the content from service-a.domain.tld!

$ curl -k -v -H 'Host: google.com' https://192.168.0.112:8443/
> GET /i HTTP/2
> Host: google.com
> user-agent: curl/7.81.0
> accept: */*
> 
< HTTP/2 200 
< date: Mon, 09 Jan 2023 14:50:36 GMT
< content-type: text/html; charset=UTF-8
< content-length: 6933
< accept-ranges: bytes
< cache-control: public, max-age=0
< last-modified: Sat, 07 Jan 2023 23:27:21 GMT
< etag: W/"1b15-1858e8f6a65"
< vary: Accept-Encoding
< strict-transport-security: max-age=31536000
< content-security-policy: object-src 'none'; form-action 'self'; frame-ancestors 'self';
< referrer-policy: strict-origin-when-cross-origin
< permissions-policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), serial=(), usb=(), web-share=(), xr-spatial-tracking=()
< feature-policy: accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; battery 'none'; camera 'none'; display-capture 'none'; document-domain 'none'; encrypted-media 'none'; execution-while-not-rendered 'none'; execution-while-out-of-viewport 'none'; fullscreen 'none';  'none'; geolocation 'none'; gyroscope 'none'; layout-animation 'none'; legacy-image-formats 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; navigation-override 'none'; payment 'none'; picture-in-picture 'none'; publickey-credentials-get 'none'; speaker-selection 'none'; sync-xhr 'none'; unoptimized-images 'none'; unsized-media 'none'; usb 'none'; screen-wake-lock 'none'; web-share 'none'; xr-spatial-tracking 'none';
< x-frame-options: SAMEORIGIN
< x-content-type-options: nosniff
< x-xss-protection: 1; mode=block
< 
<!doctype html>
<!-- html from service A -->

It also works through a browser: if I go to https://foo.selfhosting.domain.tld (and ignore the invalid SSL certificate) I get presented with the content from service-a.domain.tld. Switching the order of SERVER_NAME will serve the content of service-b.selfhosting.domain.tld.

Logs

Logs aren't very helpful unfortunately:

google.com 192.168.0.109 - - [09/Jan/2023:14:36:22 +0000] "GET /i HTTP/2.0" 200 6933 "-" "curl/7.81.0"
foo.selfhosting.domain.tld 192.168.0.1 - - [09/Jan/2023:14:37:46 +0000] "GET /i/?rid=63bc211fd713d HTTP/2.0" 200 2549 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:108.0) Gecko/20100101 Firefox/108.0"
foo.selfhosting.domain.tld 192.168.0.1 - - [09/Jan/2023:14:37:46 +0000] "GET /assets/n8n-design-system.ed27cad2.css HTTP/2.0" 304 0 "https://foo.selfhosting.domain.tld/i/?rid=63bc211fd713d" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:108.0) Gecko/20100101 Firefox/108.0"
axeleroy commented 1 year ago

Little update: this behavior seem to only happen over HTTPS. When requesting over HTTP, I get the expected HTTP 403 response:

curl -k -v -H 'Host: google.com' http://192.168.0.112:8080/
*   Trying 192.168.0.112:8080...
* Connected to 192.168.0.112 (192.168.0.112) port 8080 (#0)
> GET / HTTP/1.1
> Host: google.com
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 403 Forbidden
< Server: nginx
< Date: Wed, 08 Feb 2023 11:00:11 GMT
< Content-Type: text/html
< Content-Length: 146
< Connection: keep-alive
< 
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>
* Connection #0 to host 192.168.0.112 left intact

The behavior is exactly the same on a browser.

fl0ppy-d1sk commented 1 year ago

Hello @axeleroy,

It should be fixed in the v1.4.6 release.

Technical details : a self-signed certificates is generated and used for the default server. If HTTPS is used and DISABLE_DEFAULT_SERVER is set to yes client will get a 403 or a 444 according to DENY_HTTP_STATUS.

axeleroy commented 1 year ago

Hello @fl0ppy-d1sk,

I updated to 1.4.6 following the release yesterday and the behavior has not changed. Do I need to change anything in my Docker Compose?

fl0ppy-d1sk commented 1 year ago

Hello @axeleroy,

Something went wrong with that feature on 1.4.6. We just released 1.4.7 which should fix it (again...).

axeleroy commented 1 year ago

Hello @fl0ppy-d1sk,

It's all good, it now displays the expected behavior and throws a 403 error.