Add default headers to webserver responses (#97784)

* Add default headers to webserver responses

* Set default server header

* Fix other tests
pull/97462/head^2
Franck Nijhof 2023-08-07 05:25:13 +02:00 committed by GitHub
parent 3df71eca45
commit 369a484a78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 85 additions and 0 deletions

View File

@ -53,6 +53,7 @@ from .const import ( # noqa: F401
) )
from .cors import setup_cors from .cors import setup_cors
from .forwarded import async_setup_forwarded from .forwarded import async_setup_forwarded
from .headers import setup_headers
from .request_context import current_request, setup_request_context from .request_context import current_request, setup_request_context
from .security_filter import setup_security_filter from .security_filter import setup_security_filter
from .static import CACHE_HEADERS, CachingStaticResource from .static import CACHE_HEADERS, CachingStaticResource
@ -69,6 +70,7 @@ CONF_SSL_PEER_CERTIFICATE: Final = "ssl_peer_certificate"
CONF_SSL_KEY: Final = "ssl_key" CONF_SSL_KEY: Final = "ssl_key"
CONF_CORS_ORIGINS: Final = "cors_allowed_origins" CONF_CORS_ORIGINS: Final = "cors_allowed_origins"
CONF_USE_X_FORWARDED_FOR: Final = "use_x_forwarded_for" 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_TRUSTED_PROXIES: Final = "trusted_proxies"
CONF_LOGIN_ATTEMPTS_THRESHOLD: Final = "login_attempts_threshold" CONF_LOGIN_ATTEMPTS_THRESHOLD: Final = "login_attempts_threshold"
CONF_IP_BAN_ENABLED: Final = "ip_ban_enabled" CONF_IP_BAN_ENABLED: Final = "ip_ban_enabled"
@ -118,6 +120,7 @@ HTTP_SCHEMA: Final = vol.All(
vol.Optional(CONF_SSL_PROFILE, default=SSL_MODERN): vol.In( vol.Optional(CONF_SSL_PROFILE, default=SSL_MODERN): vol.In(
[SSL_INTERMEDIATE, SSL_MODERN] [SSL_INTERMEDIATE, SSL_MODERN]
), ),
vol.Optional(CONF_USE_X_FRAME_OPTIONS, default=True): cv.boolean,
} }
), ),
) )
@ -136,6 +139,7 @@ class ConfData(TypedDict, total=False):
ssl_key: str ssl_key: str
cors_allowed_origins: list[str] cors_allowed_origins: list[str]
use_x_forwarded_for: bool use_x_forwarded_for: bool
use_x_frame_options: bool
trusted_proxies: list[IPv4Network | IPv6Network] trusted_proxies: list[IPv4Network | IPv6Network]
login_attempts_threshold: int login_attempts_threshold: int
ip_ban_enabled: bool ip_ban_enabled: bool
@ -180,6 +184,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
ssl_key = conf.get(CONF_SSL_KEY) ssl_key = conf.get(CONF_SSL_KEY)
cors_origins = conf[CONF_CORS_ORIGINS] cors_origins = conf[CONF_CORS_ORIGINS]
use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False) 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 [] trusted_proxies = conf.get(CONF_TRUSTED_PROXIES) or []
is_ban_enabled = conf[CONF_IP_BAN_ENABLED] is_ban_enabled = conf[CONF_IP_BAN_ENABLED]
login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD] login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
@ -200,6 +205,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
use_x_forwarded_for=use_x_forwarded_for, use_x_forwarded_for=use_x_forwarded_for,
login_threshold=login_threshold, login_threshold=login_threshold,
is_ban_enabled=is_ban_enabled, is_ban_enabled=is_ban_enabled,
use_x_frame_options=use_x_frame_options,
) )
async def stop_server(event: Event) -> None: async def stop_server(event: Event) -> None:
@ -331,6 +337,7 @@ class HomeAssistantHTTP:
use_x_forwarded_for: bool, use_x_forwarded_for: bool,
login_threshold: int, login_threshold: int,
is_ban_enabled: bool, is_ban_enabled: bool,
use_x_frame_options: bool,
) -> None: ) -> None:
"""Initialize the server.""" """Initialize the server."""
self.app[KEY_HASS] = self.hass self.app[KEY_HASS] = self.hass
@ -348,6 +355,7 @@ class HomeAssistantHTTP:
await async_setup_auth(self.hass, self.app) await async_setup_auth(self.hass, self.app)
setup_headers(self.app, use_x_frame_options)
setup_cors(self.app, cors_origins) setup_cors(self.app, cors_origins)
if self.ssl_certificate: if self.ssl_certificate:

View File

@ -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)

View File

@ -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

View File

@ -117,6 +117,7 @@ def test_secrets(mock_is_file, event_loop, mock_hass_config_yaml: None) -> None:
"login_attempts_threshold": -1, "login_attempts_threshold": -1,
"server_port": 8123, "server_port": 8123,
"ssl_profile": "modern", "ssl_profile": "modern",
"use_x_frame_options": True,
} }
assert res["secret_cache"] == { assert res["secret_cache"] == {
get_test_config_dir("secrets.yaml"): {"http_pw": "http://google.com"} get_test_config_dir("secrets.yaml"): {"http_pw": "http://google.com"}