Repository: home-assistant/core
Issue category: Security Vulnerability

Security issue, unauthorized trusted access by spoofing x-forwarded-for header
**Home Assistant release with the issue:**
0.68.1 but probably older versions too

**Last working Home Assistant release (if known):**
N/A

**Operating environment (Hass.io/Docker/Windows/etc.):**
Hassbian but should be applicable to other deployment methods

**Component/platform:**
http

**Description of problem:**
There is a bug in HA that in certain conditions will allow a person to spoof a trusted source address and bypass password authentication.

Conditions:

- “trusted_networks” is defined with IP values
- “use_x_forwarded_for: True”
- A reverse proxy is used (i.e nginx)
- Attacker can guess one of the IPs in the trusted list (i.e. 127.0.0.1)
- API password is set (or else there is nothing to bypass)

Use case:
Attacker adds a spoofed x-forwarded-for header with an IP that is in the trusted list. HA grants access without asking for a password.

Cause: 
https://github.com/home-assistant/home-assistant/blob/master/homeassistant/components/http/real_ip.py#L22
Uses the first IP in the header which can be spoofed.
Only the last IP in the header is guaranteed to be non-spoofed assuming a valid reverse proxy is in place.

https://en.wikipedia.org/wiki/X-Forwarded-For : Format : 
_“where the value is a comma+space separated list of IP addresses, the left-most being the original client, and each successive proxy that passed the request adding the IP address where it received the request from. In this example, the request passed through proxy1, proxy2, and then proxy3 (not shown in the header). proxy3 appears as remote address of the request.
Since it is easy to forge an X-Forwarded-For field the given information should be used with care. The last IP address is always the IP address that connects to the last proxy, which means it is the most reliable source of information. X-Forwarded-For data can be used in a forward or reverse proxy scenario.”_

**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):**
```yaml
http:
  # Secrets are defined in the file secrets.yaml
  api_password: !secret http_password
  use_x_forwarded_for: True
  trusted_networks:
    - 127.0.0.1
```

**Traceback (if applicable):**
```

```

**Additional information:**
Work around: Disable trusted_network setting, remove reverse proxy or limit network access to authorized individuals only.

Problem first noticed first by https://community.home-assistant.io/u/NightRanger . Reference: https://community.home-assistant.io/t/trusted-networks-and-x-forwarded-for/20850 

I would issue a PR but I don’t have the right environment to test this. Hopefully somebody who uses this setup can apply the fix and test it.



Functions to edit:
homeassistant/components/http/__init__.py:HomeAssistantHTTP.__init__
homeassistant/components/http/real_ip.py:setup_real_ip

Patch:
diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py
index d8c877e83a205..f769d2bc4ffba 100644
--- a/homeassistant/components/http/__init__.py
+++ b/homeassistant/components/http/__init__.py
@@ -180,7 +180,7 @@ def __init__(self, hass, api_password,
             middlewares=[staticresource_middleware])
 
         # This order matters
-        setup_real_ip(app, use_x_forwarded_for)
+        setup_real_ip(app, use_x_forwarded_for, trusted_networks)
 
         if is_ban_enabled:
             setup_bans(hass, app, login_threshold)
diff --git a/homeassistant/components/http/real_ip.py b/homeassistant/components/http/real_ip.py
index c394016a683c4..401a09dc3066f 100644
--- a/homeassistant/components/http/real_ip.py
+++ b/homeassistant/components/http/real_ip.py
@@ -11,18 +11,22 @@
 
 
 @callback
-def setup_real_ip(app, use_x_forwarded_for):
+def setup_real_ip(app, use_x_forwarded_for, trusted_networks):
     """Create IP Ban middleware for the app."""
     @middleware
     async def real_ip_middleware(request, handler):
         """Real IP middleware."""
+        connected_ip = ip_address(
+            request.transport.get_extra_info('peername')[0])
+        request[KEY_REAL_IP] = connected_ip
+
+        # Only use the XFF header if enabled, present, and from a trusted proxy
         if (use_x_forwarded_for and
-                X_FORWARDED_FOR in request.headers):
+                X_FORWARDED_FOR in request.headers and
+                any(connected_ip in trusted_network
+                    for trusted_network in trusted_networks)):
             request[KEY_REAL_IP] = ip_address(
                 request.headers.get(X_FORWARDED_FOR).split(',')[0])
-        else:
-            request[KEY_REAL_IP] = \
-                ip_address(request.transport.get_extra_info('peername')[0])
 
         return await handler(request)
 

Test patch: diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py
index a44d17d513db9..dd8b2cd35c46c 100644
--- a/tests/components/http/test_auth.py
+++ b/tests/components/http/test_auth.py
@@ -41,7 +41,7 @@ def app():
     """Fixture to setup a web.Application."""
     app = web.Application()
     app.router.add_get('/', mock_handler)
-    setup_real_ip(app, False)
+    setup_real_ip(app, False, [])
     return app
 
 
diff --git a/tests/components/http/test_real_ip.py b/tests/components/http/test_real_ip.py
index 61846eb94c242..b6af81592078f 100644
--- a/tests/components/http/test_real_ip.py
+++ b/tests/components/http/test_real_ip.py
@@ -1,6 +1,7 @@
 """Test real IP middleware."""
 from aiohttp import web
 from aiohttp.hdrs import X_FORWARDED_FOR
+from ipaddress import ip_network
 
 from homeassistant.components.http.real_ip import setup_real_ip
 from homeassistant.components.http.const import KEY_REAL_IP
@@ -15,7 +16,7 @@ async def test_ignore_x_forwarded_for(aiohttp_client):
     """Test that we get the IP from the transport."""
     app = web.Application()
     app.router.add_get('/', mock_handler)
-    setup_real_ip(app, False)
+    setup_real_ip(app, False, [])
 
     mock_api_client = await aiohttp_client(app)
 
@@ -27,11 +28,27 @@ async def test_ignore_x_forwarded_for(aiohttp_client):
     assert text != '255.255.255.255'
 
 
-async def test_use_x_forwarded_for(aiohttp_client):
+async def test_use_x_forwarded_for_without_trusted_proxy(aiohttp_client):
     """Test that we get the IP from the transport."""
     app = web.Application()
     app.router.add_get('/', mock_handler)
-    setup_real_ip(app, True)
+    setup_real_ip(app, True, [])
+
+    mock_api_client = await aiohttp_client(app)
+
+    resp = await mock_api_client.get('/', headers={
+        X_FORWARDED_FOR: '255.255.255.255'
+    })
+    assert resp.status == 200
+    text = await resp.text()
+    assert text != '255.255.255.255'
+
+
+async def test_use_x_forwarded_for_with_trusted_proxy(aiohttp_client):
+    """Test that we get the IP from the transport."""
+    app = web.Application()
+    app.router.add_get('/', mock_handler)
+    setup_real_ip(app, True, [ip_network('127.0.0.1')])
 
     mock_api_client = await aiohttp_client(app)