check WebSocket origin

This fixes a real cross-site WebSocket hijacking (CSWSH) vulnerability.
If the attacker knows the URL of an NVR installation this user is
authenticated to and the UUID of a camera, and can trick the user into
visiting their webpage, they can grab the live stream. At least there's
some entropy in the camera UUID, but it was never intended to be a
secret.
This commit is contained in:
Scott Lamb 2022-03-22 14:28:25 -07:00
parent 307a3884a0
commit 4c9aa93fdf
2 changed files with 36 additions and 0 deletions

View File

@ -6,6 +6,12 @@ changes, see Git history.
Each release is tagged in Git and on the Docker repository
[`scottlamb/moonfire-nvr`](https://hub.docker.com/r/scottlamb/moonfire-nvr).
## unreleased
* security fix: check the `Origin` header on live stream WebSocket requests
to avoid cross-site WebSocket hijacking (CSWSH).
* RTSP connections always use the Retina library rather than FFmpeg.
## `v0.7.2` (2022-03-16)
* introduce a configuration file `/etc/moonfire-nvr.toml`; you will need

View File

@ -27,6 +27,36 @@ impl Service {
uuid: Uuid,
stream_type: db::StreamType,
) -> ResponseResult {
// Web browsers must supply origin:
// https://datatracker.ietf.org/doc/html/rfc6455#section-4.1
//
// If present, verify it. Chrome doesn't honor the `s=` cookie's
// `SameSite=Lax` setting for WebSocket requests, so this is the sole
// protection against CSWSH.
// https://christian-schneider.net/CrossSiteWebSocketHijacking.html
if let Some(origin) = req.headers().get(http::header::ORIGIN) {
let host = req
.headers()
.get(header::HOST)
.ok_or_else(|| bad_req("missing Host header"))?;
let origin = origin
.to_str()
.ok()
.and_then(|o| url::Url::parse(o).ok())
.ok_or_else(|| bad_req("bad Origin header"))?;
let origin_host = origin
.host_str()
.ok_or_else(|| bad_req("bad Origin header"))?;
if host.as_bytes() != origin_host.as_bytes() {
bail_t!(
PermissionDenied,
"cross-origin request forbidden (request host {:?}, origin host {:?})",
host,
origin_host
);
}
}
if !caller.permissions.view_video {
bail_t!(PermissionDenied, "view_video required");
}