From 0a67822d9ab66ad05ff129f4b058dff1bde9e459 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Sat, 27 Jun 2026 10:27:40 +0200 Subject: [PATCH] pick http for .onion by default --- .../weblibre/assets/preferences/settings.json | 7 +++ .../lib/utils/input_classification.dart | 8 ++++ apps/weblibre/lib/utils/uri_input_parser.dart | 14 +++++- .../test/utils/input_classification_test.dart | 43 +++++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/apps/weblibre/assets/preferences/settings.json b/apps/weblibre/assets/preferences/settings.json index 90d6d827..54eae724 100644 --- a/apps/weblibre/assets/preferences/settings.json +++ b/apps/weblibre/assets/preferences/settings.json @@ -821,6 +821,13 @@ "enforceOnStartup": true, "locked": true }, + "dom.security.https_only_mode.upgrade_onion": { + "value": false, + "title": "Keep .onion sites on HTTP", + "description": "Exempts .onion hosts from HTTPS-Only/HTTPS-First upgrades. Onion services are reached over an encrypted, self-authenticating Tor circuit, so HTTP carries no clearnet exposure and most onion sites are HTTP-only.", + "enforceOnStartup": true, + "locked": true + }, "security.mixed_content.upgrade_display_content": { "value": true, "title": "Upgrade mixed display content to HTTPS", diff --git a/apps/weblibre/lib/utils/input_classification.dart b/apps/weblibre/lib/utils/input_classification.dart index ccd75a6e..dadb8e10 100644 --- a/apps/weblibre/lib/utils/input_classification.dart +++ b/apps/weblibre/lib/utils/input_classification.dart @@ -27,6 +27,7 @@ enum NavigationReason { explicitScheme, schemelessHost, localhost, + onionHost, ipLiteral, aboutLike, } @@ -145,6 +146,13 @@ InputClassification classifyAddressBarInput( ); } + if (isOnionHost(schemelessUri.host)) { + return InputClassification.navigate( + schemelessUri, + NavigationReason.onionHost, + ); + } + if (InternetAddress.tryParse(schemelessUri.host) != null) { return InputClassification.navigate( schemelessUri, diff --git a/apps/weblibre/lib/utils/uri_input_parser.dart b/apps/weblibre/lib/utils/uri_input_parser.dart index 7f1b90db..11b508d5 100644 --- a/apps/weblibre/lib/utils/uri_input_parser.dart +++ b/apps/weblibre/lib/utils/uri_input_parser.dart @@ -114,6 +114,12 @@ bool isValidHostCandidate(String hostCandidate) { return true; } +bool isOnionHost(String host) { + // RFC 7686: ".onion" must be the final label, never a substring. The leading + // dot requirement rejects bare "onion", "notonion.com" and "onion.evil.com". + return host.toLowerCase().endsWith('.onion'); +} + bool looksLikeHostExpression(String input) { return input.contains('.') || input.contains(':') || @@ -183,7 +189,13 @@ Uri? parseSchemelessWebHost( return null; } - final scheme = probeUri.host.toLowerCase() == 'localhost' ? 'http' : 'https'; + // Onion services are self-authenticating and reached over an encrypted Tor + // circuit, so http carries no clearnet exposure; default them to http like + // localhost (see Tor Browser's HTTPS-Only exemption for .onion). + final lowerHost = probeUri.host.toLowerCase(); + final scheme = (lowerHost == 'localhost' || isOnionHost(lowerHost)) + ? 'http' + : 'https'; if (allowedSchemes != null && !allowedSchemes.contains(scheme)) { return null; } diff --git a/apps/weblibre/test/utils/input_classification_test.dart b/apps/weblibre/test/utils/input_classification_test.dart index 2dc6ed73..7fa61902 100644 --- a/apps/weblibre/test/utils/input_classification_test.dart +++ b/apps/weblibre/test/utils/input_classification_test.dart @@ -41,6 +41,49 @@ void main() { expect(navigation.uri.toString(), 'https://weblibre.eu'); }); + test('navigates onion host over http', () { + final result = classifyAddressBarInput( + 'duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion/path', + ); + + expect(result, isA()); + final navigation = result as NavigateInputClassification; + expect(navigation.reason, NavigationReason.onionHost); + expect( + navigation.uri.toString(), + 'http://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion/path', + ); + }); + + test('keeps explicit https on onion host', () { + final result = classifyAddressBarInput( + 'https://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion', + ); + + expect(result, isA()); + final navigation = result as NavigateInputClassification; + expect(navigation.reason, NavigationReason.explicitScheme); + expect(navigation.uri.scheme, 'https'); + }); + + test('does not http-default a non-onion lookalike host', () { + final result = classifyAddressBarInput('notonion.com'); + + expect(result, isA()); + final navigation = result as NavigateInputClassification; + expect(navigation.reason, NavigationReason.schemelessHost); + expect(navigation.uri.toString(), 'https://notonion.com'); + }); + + test('does not http-default onion as a non-final label', () { + final result = classifyAddressBarInput('onion.example.com'); + + expect(result, isA()); + final navigation = result as NavigateInputClassification; + expect(navigation.reason, NavigationReason.schemelessHost); + expect(navigation.uri.toString(), 'https://onion.example.com'); + }); + test('navigates localhost over http', () { final result = classifyAddressBarInput('localhost:8080');