use SameSite=Lax instead of SameSite=Strict
To improve reliability of live streams (#59) on Safari. Safari was dropping the cookie from websocket update requests. (But it worked sometimes. I don't get why.) I saw folks on the Internet thinking this related to HttpOnly: * https://developer.apple.com/forums/thread/104488 * https://stackoverflow.com/q/47742807/23584 but I still see this behavior without HttpOnly. SameSite=Strict vs SameSite=Lax appears to make a difference. Try that instead. SameSite=Strict is pointless for us anyway as noted in a new comment. Turning off HttpOnly would be more unfortunate security-wise.
This commit is contained in:
parent
2fe961f382
commit
560fe804d6
|
@ -26,7 +26,7 @@ The request should have an `application/json` body containing a dict with
|
||||||
`username` and `password` keys.
|
`username` and `password` keys.
|
||||||
|
|
||||||
On successful authentication, the server will return an HTTP 204 (no content)
|
On successful authentication, the server will return an HTTP 204 (no content)
|
||||||
with a `Set-Cookie` header for the `s` cookie, which is an opaque, HttpOnly
|
with a `Set-Cookie` header for the `s` cookie, which is an opaque, `HttpOnly`
|
||||||
(unavailable to Javascript) session identifier.
|
(unavailable to Javascript) session identifier.
|
||||||
|
|
||||||
If authentication or authorization fails, the server will return a HTTP 403
|
If authentication or authorization fails, the server will return a HTTP 403
|
||||||
|
|
|
@ -233,6 +233,7 @@ impl Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A raw session id (not base64-encoded). Sensitive. Never stored in the database.
|
/// A raw session id (not base64-encoded). Sensitive. Never stored in the database.
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
pub struct RawSessionId([u8; 48]);
|
pub struct RawSessionId([u8; 48]);
|
||||||
|
|
||||||
impl RawSessionId {
|
impl RawSessionId {
|
||||||
|
|
|
@ -7,7 +7,6 @@ use crate::json;
|
||||||
use crate::mp4;
|
use crate::mp4;
|
||||||
use base::{bail_t, ErrorKind};
|
use base::{bail_t, ErrorKind};
|
||||||
use base::{clock::Clocks, format_err_t};
|
use base::{clock::Clocks, format_err_t};
|
||||||
use bytes::{BufMut, BytesMut};
|
|
||||||
use core::borrow::Borrow;
|
use core::borrow::Borrow;
|
||||||
use core::str::FromStr;
|
use core::str::FromStr;
|
||||||
use db::dir::SampleFileDir;
|
use db::dir::SampleFileDir;
|
||||||
|
@ -1100,34 +1099,32 @@ impl Service {
|
||||||
}
|
}
|
||||||
.to_owned();
|
.to_owned();
|
||||||
let mut l = self.db.lock();
|
let mut l = self.db.lock();
|
||||||
|
|
||||||
|
// If the request came in over https, tell the browser to only send the cookie on https
|
||||||
|
// requests also.
|
||||||
let is_secure = self.is_secure(&req);
|
let is_secure = self.is_secure(&req);
|
||||||
let flags = (auth::SessionFlag::HttpOnly as i32)
|
|
||||||
| (auth::SessionFlag::SameSite as i32)
|
// Use SameSite=Lax rather than SameSite=Strict. Safari apparently doesn't send
|
||||||
| (auth::SessionFlag::SameSiteStrict as i32)
|
// SameSite=Strict cookies on WebSocket upgrade requests. There's no real security
|
||||||
|
// difference for Moonfire NVR anyway. SameSite=Strict exists as CSRF protection for
|
||||||
|
// sites that (unlike Moonfire NVR) don't follow best practices by (a)
|
||||||
|
// mutating based on GET requests and (b) not using CSRF tokens.
|
||||||
|
use auth::SessionFlag;
|
||||||
|
let flags = (SessionFlag::HttpOnly as i32)
|
||||||
|
| (SessionFlag::SameSite as i32)
|
||||||
| if is_secure {
|
| if is_secure {
|
||||||
auth::SessionFlag::Secure as i32
|
SessionFlag::Secure as i32
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
let (sid, _) = l
|
let (sid, _) = l
|
||||||
.login_by_password(authreq, &r.username, r.password, Some(domain), flags)
|
.login_by_password(authreq, &r.username, r.password, Some(domain), flags)
|
||||||
.map_err(|e| plain_response(StatusCode::UNAUTHORIZED, e.to_string()))?;
|
.map_err(|e| plain_response(StatusCode::UNAUTHORIZED, e.to_string()))?;
|
||||||
let s_suffix = if is_secure {
|
let cookie = encode_sid(sid, flags);
|
||||||
&b"; HttpOnly; Secure; SameSite=Strict; Max-Age=2147483648; Path=/"[..]
|
|
||||||
} else {
|
|
||||||
&b"; HttpOnly; SameSite=Strict; Max-Age=2147483648; Path=/"[..]
|
|
||||||
};
|
|
||||||
let mut encoded = [0u8; 64];
|
|
||||||
base64::encode_config_slice(&sid, base64::STANDARD_NO_PAD, &mut encoded);
|
|
||||||
let mut cookie = BytesMut::with_capacity("s=".len() + encoded.len() + s_suffix.len());
|
|
||||||
cookie.put(&b"s="[..]);
|
|
||||||
cookie.put(&encoded[..]);
|
|
||||||
cookie.put(s_suffix);
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header(
|
.header(
|
||||||
header::SET_COOKIE,
|
header::SET_COOKIE,
|
||||||
HeaderValue::from_maybe_shared(cookie.freeze())
|
HeaderValue::try_from(cookie).expect("cookie can't have invalid bytes"),
|
||||||
.expect("cookie can't have invalid bytes"),
|
|
||||||
)
|
)
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::NO_CONTENT)
|
||||||
.body(b""[..].into())
|
.body(b""[..].into())
|
||||||
|
@ -1295,6 +1292,27 @@ impl Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Encodes a session into `Set-Cookie` header value form.
|
||||||
|
fn encode_sid(sid: db::RawSessionId, flags: i32) -> String {
|
||||||
|
let mut cookie = String::with_capacity(128);
|
||||||
|
cookie.push_str("s=");
|
||||||
|
base64::encode_config_buf(&sid, base64::STANDARD_NO_PAD, &mut cookie);
|
||||||
|
use auth::SessionFlag;
|
||||||
|
if (flags & SessionFlag::HttpOnly as i32) != 0 {
|
||||||
|
cookie.push_str("; HttpOnly");
|
||||||
|
}
|
||||||
|
if (flags & SessionFlag::Secure as i32) != 0 {
|
||||||
|
cookie.push_str("; Secure");
|
||||||
|
}
|
||||||
|
if (flags & SessionFlag::SameSiteStrict as i32) != 0 {
|
||||||
|
cookie.push_str("; SameSite=Strict");
|
||||||
|
} else if (flags & SessionFlag::SameSite as i32) != 0 {
|
||||||
|
cookie.push_str("; SameSite=Lax");
|
||||||
|
}
|
||||||
|
cookie.push_str("; Max-Age=2147483648; Path=/");
|
||||||
|
cookie
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
struct StaticFileRequest<'a> {
|
struct StaticFileRequest<'a> {
|
||||||
path: &'a str,
|
path: &'a str,
|
||||||
|
@ -1730,6 +1748,31 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST);
|
assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encode_sid() {
|
||||||
|
use super::encode_sid;
|
||||||
|
use db::auth::{RawSessionId, SessionFlag};
|
||||||
|
let s64 = "3LbrruP5vj/hpE8kvYTz/rNDg4BleRiTCHGA3Ocm91z/YrtxHDxexmrz46biZJxJ";
|
||||||
|
let s = RawSessionId::decode_base64(s64.as_bytes()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
encode_sid(
|
||||||
|
s,
|
||||||
|
(SessionFlag::Secure as i32)
|
||||||
|
| (SessionFlag::HttpOnly as i32)
|
||||||
|
| (SessionFlag::SameSite as i32)
|
||||||
|
| (SessionFlag::SameSiteStrict as i32)
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"s={}; HttpOnly; Secure; SameSite=Strict; Max-Age=2147483648; Path=/",
|
||||||
|
s64
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
encode_sid(s, SessionFlag::SameSite as i32),
|
||||||
|
format!("s={}; SameSite=Lax; Max-Age=2147483648; Path=/", s64)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(test, feature = "nightly"))]
|
#[cfg(all(test, feature = "nightly"))]
|
||||||
|
|
|
@ -21,24 +21,13 @@ module.exports = (app) => {
|
||||||
// this attribute in the proxy with code from here:
|
// this attribute in the proxy with code from here:
|
||||||
// https://github.com/chimurai/http-proxy-middleware/issues/169#issuecomment-575027907
|
// https://github.com/chimurai/http-proxy-middleware/issues/169#issuecomment-575027907
|
||||||
// See also discussion in guide/developing-ui.md.
|
// See also discussion in guide/developing-ui.md.
|
||||||
//
|
|
||||||
// Additionally, Safari appears to (sometimes?) prevent http-only cookies
|
|
||||||
// (meaning cookies that Javascript shouldn't be able to access) from
|
|
||||||
// being passed to WebSocket requests (possibly only when not using
|
|
||||||
// https/wss). Also strip HttpOnly when using Safari.
|
|
||||||
// https://developer.apple.com/forums/thread/104488
|
|
||||||
onProxyRes: (proxyRes, req, res) => {
|
onProxyRes: (proxyRes, req, res) => {
|
||||||
const sc = proxyRes.headers["set-cookie"];
|
const sc = proxyRes.headers["set-cookie"];
|
||||||
if (Array.isArray(sc)) {
|
if (Array.isArray(sc)) {
|
||||||
proxyRes.headers["set-cookie"] = sc.map((sc) => {
|
proxyRes.headers["set-cookie"] = sc.map((sc) => {
|
||||||
return sc
|
return sc
|
||||||
.split(";")
|
.split(";")
|
||||||
.filter(
|
.filter((v) => v.trim().toLowerCase() !== "secure")
|
||||||
(v) =>
|
|
||||||
v.trim().toLowerCase() !== "secure" &&
|
|
||||||
(v.trim().toLowerCase() !== "httponly" ||
|
|
||||||
!req.headers["user-agent"].includes("Safari"))
|
|
||||||
)
|
|
||||||
.join("; ");
|
.join("; ");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue