pick http for .onion by default

This commit is contained in:
Fabian Freund
2026-06-27 10:27:40 +02:00
parent f6930a7c14
commit 0a67822d9a
4 changed files with 71 additions and 1 deletions
@@ -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",
@@ -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,
+13 -1
View File
@@ -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;
}
@@ -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<NavigateInputClassification>());
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<NavigateInputClassification>());
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<NavigateInputClassification>());
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<NavigateInputClassification>());
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');