fix: handle cookies correctly on onion mirrors

Match cookie domain to the current request host so onion mirrors fall back to host-only cookies instead of reusing the configured clearnet domain.

Also clear SID and PREFS cookies with explicit domain and path attributes to avoid leaving stale cookies behind when signing out or replacing anonymous preferences.

Refs: #1421
This commit is contained in:
khanhsnd 2026-05-11 21:44:09 +07:00
parent f914ce8040
commit 4aecf4e6f0
6 changed files with 73 additions and 18 deletions

View File

@ -3,6 +3,24 @@ require "../spec_helper"
CONFIG = Config.from_yaml(File.open("config/config.example.yml")) CONFIG = Config.from_yaml(File.open("config/config.example.yml"))
Spectator.describe "Helper" do Spectator.describe "Helper" do
describe "#cookie_domain" do
it "keeps the configured cookie domain for the matching host" do
expect(cookie_domain("example.com", "example.com")).to eq("example.com")
expect(cookie_domain("example.com", "example.com:3000")).to eq("example.com")
end
it "falls back to host-only cookies for unrelated hosts such as onion mirrors" do
expect(cookie_domain("example.com", "exampleonionaddress.onion")).to be_nil
expect(cookie_domain("example.com", "other.example")).to be_nil
end
it "preserves shared-subdomain deployments when the configured domain starts with a dot" do
expect(cookie_domain(".example.com", "example.com")).to eq(".example.com")
expect(cookie_domain(".example.com", "www.example.com")).to eq(".example.com")
expect(cookie_domain(".example.com", "deep.www.example.com")).to eq(".example.com")
end
end
describe "#produce_channel_search_continuation" do describe "#produce_channel_search_continuation" do
it "correctly produces token for searching a specific channel" do it "correctly produces token for searching a specific channel" do
expect(produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100)).to eq("4qmFsgJqEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FUZ0JZQUY2QkVkS2IxaTRBUUE9WgCaAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D") expect(produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100)).to eq("4qmFsgJqEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FUZ0JZQUY2QkVkS2IxaTRBUUE9WgCaAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D")

View File

@ -279,6 +279,26 @@ def sha256(text)
return digest.final.hexstring return digest.final.hexstring
end end
def cookie_domain(configured_domain : String?, request_host : String?) : String?
return nil unless configured_domain
return nil unless request_host
normalized_domain = configured_domain.downcase.lchop(".")
normalized_host = request_host.downcase.sub(/:\d+\z/, "")
return configured_domain if normalized_host == normalized_domain
if configured_domain.starts_with?(".") && normalized_host.ends_with?(".#{normalized_domain}")
return configured_domain
end
nil
end
def cookie_domain(env : HTTP::Server::Context) : String?
cookie_domain(CONFIG.domain, env.request.headers["Host"]?)
end
def subscribe_pubsub(topic, key) def subscribe_pubsub(topic, key)
case topic case topic
when .match(/^UC[A-Za-z0-9_-]{22}$/) when .match(/^UC[A-Za-z0-9_-]{22}$/)

View File

@ -128,10 +128,9 @@ module Invidious::Routes::Account
Invidious::Database::SessionIDs.delete(email: user.email) Invidious::Database::SessionIDs.delete(email: user.email)
PG_DB.exec("DROP MATERIALIZED VIEW #{view_name}") PG_DB.exec("DROP MATERIALIZED VIEW #{view_name}")
env.request.cookies.each do |cookie| domain = cookie_domain(env)
cookie.expires = Time.utc(1990, 1, 1) env.response.cookies["SID"] = Invidious::User::Cookies.clear_sid(domain)
env.response.cookies << cookie env.response.cookies["PREFS"] = Invidious::User::Cookies.clear_prefs(domain)
end
env.redirect referer env.redirect referer
end end

View File

@ -57,16 +57,14 @@ module Invidious::Routes::Login
sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
Invidious::Database::SessionIDs.insert(sid, email) Invidious::Database::SessionIDs.insert(sid, email)
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid) env.response.cookies["SID"] = Invidious::User::Cookies.sid(cookie_domain(env), sid)
else else
return error_template(401, "Wrong username or password") return error_template(401, "Wrong username or password")
end end
# Since this user has already registered, we don't want to overwrite their preferences # Since this user has already registered, we don't want to overwrite their preferences
if env.request.cookies["PREFS"]? if env.request.cookies["PREFS"]?
cookie = env.request.cookies["PREFS"] env.response.cookies["PREFS"] = Invidious::User::Cookies.clear_prefs(cookie_domain(env))
cookie.expires = Time.utc(1990, 1, 1)
env.response.cookies << cookie
end end
else else
if !CONFIG.registration_enabled if !CONFIG.registration_enabled
@ -123,15 +121,13 @@ module Invidious::Routes::Login
view_name = "subscriptions_#{sha256(user.email)}" view_name = "subscriptions_#{sha256(user.email)}"
PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}") PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}")
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid) env.response.cookies["SID"] = Invidious::User::Cookies.sid(cookie_domain(env), sid)
if env.request.cookies["PREFS"]? if env.request.cookies["PREFS"]?
user.preferences = env.get("preferences").as(Preferences) user.preferences = env.get("preferences").as(Preferences)
Invidious::Database::Users.update_preferences(user) Invidious::Database::Users.update_preferences(user)
cookie = env.request.cookies["PREFS"] env.response.cookies["PREFS"] = Invidious::User::Cookies.clear_prefs(cookie_domain(env))
cookie.expires = Time.utc(1990, 1, 1)
env.response.cookies << cookie
end end
end end
@ -164,10 +160,9 @@ module Invidious::Routes::Login
Invidious::Database::SessionIDs.delete(sid: sid) Invidious::Database::SessionIDs.delete(sid: sid)
env.request.cookies.each do |cookie| domain = cookie_domain(env)
cookie.expires = Time.utc(1990, 1, 1) env.response.cookies["SID"] = Invidious::User::Cookies.clear_sid(domain)
env.response.cookies << cookie env.response.cookies["PREFS"] = Invidious::User::Cookies.clear_prefs(domain)
end
env.redirect referer env.redirect referer
end end

View File

@ -226,7 +226,7 @@ module Invidious::Routes::PreferencesRoute
File.write("config/config.yml", CONFIG.to_yaml) File.write("config/config.yml", CONFIG.to_yaml)
end end
else else
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences) env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(cookie_domain(env), preferences)
end end
env.redirect referer env.redirect referer
@ -261,7 +261,7 @@ module Invidious::Routes::PreferencesRoute
preferences.dark_mode = "dark" preferences.dark_mode = "dark"
end end
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences) env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(cookie_domain(env), preferences)
end end
if redirect if redirect

View File

@ -14,6 +14,7 @@ struct Invidious::User
return HTTP::Cookie.new( return HTTP::Cookie.new(
name: "SID", name: "SID",
domain: domain, domain: domain,
path: "/",
value: sid, value: sid,
expires: Time.utc + 2.years, expires: Time.utc + 2.years,
secure: SECURE, secure: SECURE,
@ -28,6 +29,7 @@ struct Invidious::User
return HTTP::Cookie.new( return HTTP::Cookie.new(
name: "PREFS", name: "PREFS",
domain: domain, domain: domain,
path: "/",
value: URI.encode_www_form(preferences.to_json), value: URI.encode_www_form(preferences.to_json),
expires: Time.utc + 2.years, expires: Time.utc + 2.years,
secure: SECURE, secure: SECURE,
@ -35,5 +37,26 @@ struct Invidious::User
samesite: HTTP::Cookie::SameSite::Lax samesite: HTTP::Cookie::SameSite::Lax
) )
end end
def clear_sid(domain : String?) : HTTP::Cookie
clear("SID", domain, http_only: true)
end
def clear_prefs(domain : String?) : HTTP::Cookie
clear("PREFS", domain, http_only: false)
end
private def clear(name : String, domain : String?, http_only : Bool) : HTTP::Cookie
return HTTP::Cookie.new(
name: name,
domain: domain,
path: "/",
value: "",
expires: Time.utc(1990, 1, 1),
secure: SECURE,
http_only: http_only,
samesite: HTTP::Cookie::SameSite::Lax
)
end
end end
end end