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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct RawSessionId([u8; 48]);
|
||||
|
||||
impl RawSessionId {
|
||||
|
|
|
@ -7,7 +7,6 @@ use crate::json;
|
|||
use crate::mp4;
|
||||
use base::{bail_t, ErrorKind};
|
||||
use base::{clock::Clocks, format_err_t};
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use core::borrow::Borrow;
|
||||
use core::str::FromStr;
|
||||
use db::dir::SampleFileDir;
|
||||
|
@ -1100,34 +1099,32 @@ impl Service {
|
|||
}
|
||||
.to_owned();
|
||||
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 flags = (auth::SessionFlag::HttpOnly as i32)
|
||||
| (auth::SessionFlag::SameSite as i32)
|
||||
| (auth::SessionFlag::SameSiteStrict as i32)
|
||||
|
||||
// Use SameSite=Lax rather than SameSite=Strict. Safari apparently doesn't send
|
||||
// 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 {
|
||||
auth::SessionFlag::Secure as i32
|
||||
SessionFlag::Secure as i32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let (sid, _) = l
|
||||
.login_by_password(authreq, &r.username, r.password, Some(domain), flags)
|
||||
.map_err(|e| plain_response(StatusCode::UNAUTHORIZED, e.to_string()))?;
|
||||
let s_suffix = if is_secure {
|
||||
&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);
|
||||
let cookie = encode_sid(sid, flags);
|
||||
Ok(Response::builder()
|
||||
.header(
|
||||
header::SET_COOKIE,
|
||||
HeaderValue::from_maybe_shared(cookie.freeze())
|
||||
.expect("cookie can't have invalid bytes"),
|
||||
HeaderValue::try_from(cookie).expect("cookie can't have invalid bytes"),
|
||||
)
|
||||
.status(StatusCode::NO_CONTENT)
|
||||
.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)]
|
||||
struct StaticFileRequest<'a> {
|
||||
path: &'a str,
|
||||
|
@ -1730,6 +1748,31 @@ mod tests {
|
|||
.unwrap();
|
||||
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"))]
|
||||
|
|
|
@ -21,24 +21,13 @@ module.exports = (app) => {
|
|||
// this attribute in the proxy with code from here:
|
||||
// https://github.com/chimurai/http-proxy-middleware/issues/169#issuecomment-575027907
|
||||
// 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) => {
|
||||
const sc = proxyRes.headers["set-cookie"];
|
||||
if (Array.isArray(sc)) {
|
||||
proxyRes.headers["set-cookie"] = sc.map((sc) => {
|
||||
return sc
|
||||
.split(";")
|
||||
.filter(
|
||||
(v) =>
|
||||
v.trim().toLowerCase() !== "secure" &&
|
||||
(v.trim().toLowerCase() !== "httponly" ||
|
||||
!req.headers["user-agent"].includes("Safari"))
|
||||
)
|
||||
.filter((v) => v.trim().toLowerCase() !== "secure")
|
||||
.join("; ");
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue