use http::HeaderValue;
use http::header::{HeaderMap, SET_COOKIE};
pub(crate) fn strip_cookie_domains(headers: &mut HeaderMap, domain: &str) {
if let http::header::Entry::Occupied(mut entry) = headers.entry(SET_COOKIE) {
for value in entry.iter_mut() {
if let Some(stripped) = strip_domain(value.as_bytes(), domain)
&& let Ok(stripped) = HeaderValue::from_bytes(&stripped)
{
*value = stripped;
}
}
}
}
fn strip_domain(cookie: &[u8], domain: &str) -> Option<Vec<u8>> {
let domain = domain.trim_start_matches('.').to_ascii_lowercase();
let mut parts = cookie.split(|&byte| byte == b';');
let mut kept = vec![parts.next()?];
let mut stripped = false;
for attribute in parts {
if is_domain_for(attribute, &domain) {
stripped = true;
} else {
kept.push(attribute);
}
}
stripped.then(|| kept.join(&b';'))
}
fn is_domain_for(attribute: &[u8], domain: &str) -> bool {
let Ok(attribute) = std::str::from_utf8(attribute) else {
return false;
};
let Some((name, value)) = attribute.split_once('=') else {
return false;
};
let value = value.trim().trim_start_matches('.').to_ascii_lowercase();
name.trim().eq_ignore_ascii_case("domain")
&& !value.is_empty()
&& (domain == value || domain.ends_with(&format!(".{value}")))
}
#[cfg(test)]
mod tests {
use super::*;
fn stripped(cookie: &str, domain: &str) -> Option<String> {
strip_domain(cookie.as_bytes(), domain).map(|bytes| String::from_utf8(bytes).unwrap())
}
fn strip(cookie: &str) -> String {
stripped(cookie, "share.example").unwrap_or_else(|| cookie.to_string())
}
#[test]
fn strips_the_share_domain() {
assert_eq!(
strip("session=abc; Domain=share.example; Path=/"),
"session=abc; Path=/"
);
}
#[test]
fn strips_with_a_leading_dot() {
assert_eq!(
strip("session=abc; Domain=.share.example; Path=/"),
"session=abc; Path=/"
);
}
#[test]
fn matches_attribute_and_domain_case_insensitively() {
assert_eq!(strip("a=1; DOMAIN=Share.Example"), "a=1");
assert_eq!(strip("a=1; domain=.SHARE.example; secure"), "a=1; secure");
}
#[test]
fn tolerates_whitespace_around_the_attribute() {
assert_eq!(strip("a=1; Domain = share.example ;Path=/"), "a=1;Path=/");
}
#[test]
fn preserves_other_attributes_in_order() {
assert_eq!(
strip(
"id=x; Path=/app; Domain=share.example; Expires=Wed, 21 Oct 2026 07:28:00 GMT; \
Max-Age=60; Secure; HttpOnly; SameSite=Lax"
),
"id=x; Path=/app; Expires=Wed, 21 Oct 2026 07:28:00 GMT; Max-Age=60; Secure; \
HttpOnly; SameSite=Lax"
);
}
#[test]
fn strips_parents_of_the_share_domain() {
assert_eq!(
stripped("session=abc; Domain=example.com; Path=/", "dev.example.com").as_deref(),
Some("session=abc; Path=/")
);
assert_eq!(strip("a=1; Domain=.Example"), "a=1");
}
#[test]
fn keeps_other_domains() {
for cookie in [
"a=1; Domain=app--feature-x--nova.share.example",
"a=1; Domain=ample",
"a=1; Domain=e.example",
"a=1; Domain=",
"a=1; Domain=notshare.example",
"a=1; Domain=share.example.evil",
"a=1",
] {
assert_eq!(stripped(cookie, "share.example"), None, "{cookie}");
}
}
#[test]
fn leaves_the_cookie_value_alone() {
assert_eq!(
strip("domain=share.example; Domain=share.example"),
"domain=share.example"
);
}
#[test]
fn strips_every_matching_attribute() {
assert_eq!(
strip("a=1; Domain=share.example; Domain=.share.example; Path=/"),
"a=1; Path=/"
);
}
#[test]
fn accepts_a_configured_domain_with_a_leading_dot() {
assert_eq!(
stripped("a=1; Domain=share.example", ".share.example").as_deref(),
Some("a=1")
);
}
#[test]
fn strips_from_cookies_with_bytes_outside_ascii() {
let mut headers = HeaderMap::new();
headers.append(
SET_COOKIE,
HeaderValue::from_bytes(b"a=\xc3\xa6; Domain=example; Path=/").unwrap(),
);
strip_cookie_domains(&mut headers, "share.example");
assert_eq!(headers[SET_COOKIE].as_bytes(), b"a=\xc3\xa6; Path=/");
}
#[test]
fn rewrites_every_set_cookie_header() {
let mut headers = HeaderMap::new();
headers.append(
SET_COOKIE,
HeaderValue::from_static("a=1; Domain=share.example; Path=/"),
);
headers.append(SET_COOKIE, HeaderValue::from_static("b=2; Path=/"));
headers.append(
SET_COOKIE,
HeaderValue::from_static("c=3; domain=.SHARE.EXAMPLE"),
);
headers.insert("x-other", HeaderValue::from_static("Domain=share.example"));
strip_cookie_domains(&mut headers, "share.example");
let cookies: Vec<_> = headers.get_all(SET_COOKIE).iter().collect();
assert_eq!(cookies, ["a=1; Path=/", "b=2; Path=/", "c=3"]);
assert_eq!(headers["x-other"], "Domain=share.example");
}
}