From 4aecf4e6f00e3d08e2c3f65fff79748e677de631 Mon Sep 17 00:00:00 2001 From: khanhsnd Date: Mon, 11 May 2026 21:44:09 +0700 Subject: [PATCH] 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 --- spec/invidious/helpers_spec.cr | 18 ++++++++++++++++++ src/invidious/helpers/utils.cr | 20 ++++++++++++++++++++ src/invidious/routes/account.cr | 7 +++---- src/invidious/routes/login.cr | 19 +++++++------------ src/invidious/routes/preferences.cr | 4 ++-- src/invidious/user/cookies.cr | 23 +++++++++++++++++++++++ 6 files changed, 73 insertions(+), 18 deletions(-) diff --git a/spec/invidious/helpers_spec.cr b/spec/invidious/helpers_spec.cr index 9fbb6d6fe..eef029eb7 100644 --- a/spec/invidious/helpers_spec.cr +++ b/spec/invidious/helpers_spec.cr @@ -3,6 +3,24 @@ require "../spec_helper" CONFIG = Config.from_yaml(File.open("config/config.example.yml")) 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 it "correctly produces token for searching a specific channel" do expect(produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100)).to eq("4qmFsgJqEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FUZ0JZQUY2QkVkS2IxaTRBUUE9WgCaAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D") diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 7a262f849..7901aa50d 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -279,6 +279,26 @@ def sha256(text) return digest.final.hexstring 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) case topic when .match(/^UC[A-Za-z0-9_-]{22}$/) diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index c8db207c2..554da3753 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -128,10 +128,9 @@ module Invidious::Routes::Account Invidious::Database::SessionIDs.delete(email: user.email) PG_DB.exec("DROP MATERIALIZED VIEW #{view_name}") - env.request.cookies.each do |cookie| - cookie.expires = Time.utc(1990, 1, 1) - env.response.cookies << cookie - end + domain = cookie_domain(env) + env.response.cookies["SID"] = Invidious::User::Cookies.clear_sid(domain) + env.response.cookies["PREFS"] = Invidious::User::Cookies.clear_prefs(domain) env.redirect referer end diff --git a/src/invidious/routes/login.cr b/src/invidious/routes/login.cr index 01a199d32..80ebbc4a0 100644 --- a/src/invidious/routes/login.cr +++ b/src/invidious/routes/login.cr @@ -57,16 +57,14 @@ module Invidious::Routes::Login sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) 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 return error_template(401, "Wrong username or password") end # Since this user has already registered, we don't want to overwrite their preferences if env.request.cookies["PREFS"]? - cookie = env.request.cookies["PREFS"] - cookie.expires = Time.utc(1990, 1, 1) - env.response.cookies << cookie + env.response.cookies["PREFS"] = Invidious::User::Cookies.clear_prefs(cookie_domain(env)) end else if !CONFIG.registration_enabled @@ -123,15 +121,13 @@ module Invidious::Routes::Login view_name = "subscriptions_#{sha256(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"]? user.preferences = env.get("preferences").as(Preferences) Invidious::Database::Users.update_preferences(user) - cookie = env.request.cookies["PREFS"] - cookie.expires = Time.utc(1990, 1, 1) - env.response.cookies << cookie + env.response.cookies["PREFS"] = Invidious::User::Cookies.clear_prefs(cookie_domain(env)) end end @@ -164,10 +160,9 @@ module Invidious::Routes::Login Invidious::Database::SessionIDs.delete(sid: sid) - env.request.cookies.each do |cookie| - cookie.expires = Time.utc(1990, 1, 1) - env.response.cookies << cookie - end + domain = cookie_domain(env) + env.response.cookies["SID"] = Invidious::User::Cookies.clear_sid(domain) + env.response.cookies["PREFS"] = Invidious::User::Cookies.clear_prefs(domain) env.redirect referer end diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr index d9fad1b18..86639371e 100644 --- a/src/invidious/routes/preferences.cr +++ b/src/invidious/routes/preferences.cr @@ -226,7 +226,7 @@ module Invidious::Routes::PreferencesRoute File.write("config/config.yml", CONFIG.to_yaml) end 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 env.redirect referer @@ -261,7 +261,7 @@ module Invidious::Routes::PreferencesRoute preferences.dark_mode = "dark" 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 if redirect diff --git a/src/invidious/user/cookies.cr b/src/invidious/user/cookies.cr index 654efc15b..c23120e2c 100644 --- a/src/invidious/user/cookies.cr +++ b/src/invidious/user/cookies.cr @@ -14,6 +14,7 @@ struct Invidious::User return HTTP::Cookie.new( name: "SID", domain: domain, + path: "/", value: sid, expires: Time.utc + 2.years, secure: SECURE, @@ -28,6 +29,7 @@ struct Invidious::User return HTTP::Cookie.new( name: "PREFS", domain: domain, + path: "/", value: URI.encode_www_form(preferences.to_json), expires: Time.utc + 2.years, secure: SECURE, @@ -35,5 +37,26 @@ struct Invidious::User samesite: HTTP::Cookie::SameSite::Lax ) 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