Skip to content

Commit

Permalink
Add default headers to webserver responses (#97784)
Browse files Browse the repository at this point in the history
* Add default headers to webserver responses

* Set default server header

* Fix other tests
  • Loading branch information
frenck committed Aug 7, 2023
1 parent 3df71ec commit 369a484
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 0 deletions.
8 changes: 8 additions & 0 deletions homeassistant/components/http/__init__.py
Expand Up @@ -53,6 +53,7 @@
)
from .cors import setup_cors
from .forwarded import async_setup_forwarded
from .headers import setup_headers
from .request_context import current_request, setup_request_context
from .security_filter import setup_security_filter
from .static import CACHE_HEADERS, CachingStaticResource
Expand All @@ -69,6 +70,7 @@
CONF_SSL_KEY: Final = "ssl_key"
CONF_CORS_ORIGINS: Final = "cors_allowed_origins"
CONF_USE_X_FORWARDED_FOR: Final = "use_x_forwarded_for"
CONF_USE_X_FRAME_OPTIONS: Final = "use_x_frame_options"
CONF_TRUSTED_PROXIES: Final = "trusted_proxies"
CONF_LOGIN_ATTEMPTS_THRESHOLD: Final = "login_attempts_threshold"
CONF_IP_BAN_ENABLED: Final = "ip_ban_enabled"
Expand Down Expand Up @@ -118,6 +120,7 @@
vol.Optional(CONF_SSL_PROFILE, default=SSL_MODERN): vol.In(
[SSL_INTERMEDIATE, SSL_MODERN]
),
vol.Optional(CONF_USE_X_FRAME_OPTIONS, default=True): cv.boolean,
}
),
)
Expand All @@ -136,6 +139,7 @@ class ConfData(TypedDict, total=False):
ssl_key: str
cors_allowed_origins: list[str]
use_x_forwarded_for: bool
use_x_frame_options: bool
trusted_proxies: list[IPv4Network | IPv6Network]
login_attempts_threshold: int
ip_ban_enabled: bool
Expand Down Expand Up @@ -180,6 +184,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
ssl_key = conf.get(CONF_SSL_KEY)
cors_origins = conf[CONF_CORS_ORIGINS]
use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False)
use_x_frame_options = conf[CONF_USE_X_FRAME_OPTIONS]
trusted_proxies = conf.get(CONF_TRUSTED_PROXIES) or []
is_ban_enabled = conf[CONF_IP_BAN_ENABLED]
login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
Expand All @@ -200,6 +205,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
use_x_forwarded_for=use_x_forwarded_for,
login_threshold=login_threshold,
is_ban_enabled=is_ban_enabled,
use_x_frame_options=use_x_frame_options,
)

async def stop_server(event: Event) -> None:
Expand Down Expand Up @@ -331,6 +337,7 @@ async def async_initialize(
use_x_forwarded_for: bool,
login_threshold: int,
is_ban_enabled: bool,
use_x_frame_options: bool,
) -> None:
"""Initialize the server."""
self.app[KEY_HASS] = self.hass
Expand All @@ -348,6 +355,7 @@ async def async_initialize(

await async_setup_auth(self.hass, self.app)

setup_headers(self.app, use_x_frame_options)
setup_cors(self.app, cors_origins)

if self.ssl_certificate:
Expand Down
32 changes: 32 additions & 0 deletions homeassistant/components/http/headers.py
@@ -0,0 +1,32 @@
"""Middleware that helps with the control of headers in our responses."""
from __future__ import annotations

from collections.abc import Awaitable, Callable

from aiohttp.web import Application, Request, StreamResponse, middleware

from homeassistant.core import callback


@callback
def setup_headers(app: Application, use_x_frame_options: bool) -> None:
"""Create headers middleware for the app."""

@middleware
async def headers_middleware(
request: Request, handler: Callable[[Request], Awaitable[StreamResponse]]
) -> StreamResponse:
"""Process request and add headers to the responses."""
response = await handler(request)
response.headers["Referrer-Policy"] = "no-referrer"
response.headers["X-Content-Type-Options"] = "nosniff"

# Set an empty server header, to prevent aiohttp of setting one.
response.headers["Server"] = ""

if use_x_frame_options:
response.headers["X-Frame-Options"] = "SAMEORIGIN"

return response

app.middlewares.append(headers_middleware)
44 changes: 44 additions & 0 deletions tests/components/http/test_headers.py
@@ -0,0 +1,44 @@
"""Test headers middleware."""
from http import HTTPStatus

from aiohttp import web

from homeassistant.components.http.headers import setup_headers

from tests.typing import ClientSessionGenerator


async def mock_handler(request):
"""Return OK."""
return web.Response(text="OK")


async def test_headers_added(aiohttp_client: ClientSessionGenerator) -> None:
"""Test that headers are being added on each request."""
app = web.Application()
app.router.add_get("/", mock_handler)

setup_headers(app, use_x_frame_options=True)

mock_api_client = await aiohttp_client(app)
resp = await mock_api_client.get("/")

assert resp.status == HTTPStatus.OK
assert resp.headers["Referrer-Policy"] == "no-referrer"
assert resp.headers["Server"] == ""
assert resp.headers["X-Content-Type-Options"] == "nosniff"
assert resp.headers["X-Frame-Options"] == "SAMEORIGIN"


async def test_allow_framing(aiohttp_client: ClientSessionGenerator) -> None:
"""Test that we allow framing when disabled."""
app = web.Application()
app.router.add_get("/", mock_handler)

setup_headers(app, use_x_frame_options=False)

mock_api_client = await aiohttp_client(app)
resp = await mock_api_client.get("/")

assert resp.status == HTTPStatus.OK
assert "X-Frame-Options" not in resp.headers
1 change: 1 addition & 0 deletions tests/scripts/test_check_config.py
Expand Up @@ -117,6 +117,7 @@ def test_secrets(mock_is_file, event_loop, mock_hass_config_yaml: None) -> None:
"login_attempts_threshold": -1,
"server_port": 8123,
"ssl_profile": "modern",
"use_x_frame_options": True,
}
assert res["secret_cache"] == {
get_test_config_dir("secrets.yaml"): {"http_pw": "http://google.com"}
Expand Down

0 comments on commit 369a484

Please sign in to comment.