Skip to content

Commit d878cfb

Browse files
mehmetfcommit-bot@chromium.org
authored andcommitted
[dart:io] Introduce per-domain policy for strict secure connections.
The default behavior is controlled via a private configuration variable settable by embedders (#_mayUseInsecureSocket). It is, by default, configured to allow insecure connections. The domain configuration itself is sent as a JSON encoded string which gets parsed once. Embedders are expected to set these configurations before they run any user code. This is a re-work of https://dart-review.googlesource.com/c/sdk/+/142446 [dart:_http] Allow the embedder to prohibit HTTP traffic. Change-Id: I4ccbd35da9ce25bf5f81ad4468111018d6af2f03 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/154180 Commit-Queue: Mehmet Fidanboylu <[email protected]> Reviewed-by: Jonas Termansen <[email protected]>
1 parent 3a82163 commit d878cfb

22 files changed

+511
-261
lines changed

sdk/lib/_http/embedder_config.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,9 @@ part of dart._http;
77
/// Embedder-specific `dart:_http` configuration.
88
99
/// [HttpClient] will disallow HTTP URLs if this value is set to `false`.
10+
///
11+
/// TODO(https://github.com/dart-lang/sdk/issues/41796): This setting will be
12+
/// removed in favor of explicit domain settings.
13+
@deprecated
1014
@pragma("vm:entry-point")
1115
bool _embedderAllowsHttp = true;

sdk/lib/_http/http_impl.dart

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2271,32 +2271,6 @@ class _HttpClient implements HttpClient {
22712271
});
22722272
}
22732273

2274-
/// Whether HTTP requests are currently allowed.
2275-
///
2276-
/// If the [Zone] variable `#dart.library.io.allow_http` is set to a boolean,
2277-
/// it determines whether the HTTP protocol is allowed. If the zone variable
2278-
/// is set to any other non-null value, HTTP is not allowed.
2279-
/// Otherwise, if the `dart.library.io.allow_http` environment flag
2280-
/// is set to `false`, HTTP is not allowed.
2281-
/// Otherwise, [_embedderAllowsHttp] determines the result.
2282-
bool get _isHttpAllowed {
2283-
final zoneOverride = Zone.current[#dart.library.io.allow_http];
2284-
if (zoneOverride != null) return true == zoneOverride;
2285-
bool envOverride =
2286-
bool.fromEnvironment("dart.library.io.allow_http", defaultValue: true);
2287-
return envOverride && _embedderAllowsHttp;
2288-
}
2289-
2290-
bool _isLoopback(String host) {
2291-
if (host.isEmpty) return false;
2292-
if ("localhost" == host) return true;
2293-
try {
2294-
return InternetAddress(host).isLoopback;
2295-
} on ArgumentError {
2296-
return false;
2297-
}
2298-
}
2299-
23002274
Future<_HttpClientRequest> _openUrl(String method, Uri uri) {
23012275
if (_closing) {
23022276
throw new StateError("Client is closed");
@@ -2318,11 +2292,6 @@ class _HttpClient implements HttpClient {
23182292
}
23192293

23202294
bool isSecure = uri.isScheme("https");
2321-
if (!_isHttpAllowed && !isSecure && !_isLoopback(uri.host)) {
2322-
throw new StateError(
2323-
"Insecure HTTP is not allowed by the current platform: $uri");
2324-
}
2325-
23262295
int port = uri.port;
23272296
if (port == 0) {
23282297
port =

sdk/lib/io/embedder_config.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ abstract class _EmbedderConfig {
3232
@pragma('vm:entry-point')
3333
static bool _maySleep = true;
3434

35+
/// The Isolate may establish insecure socket connections to all domains.
36+
///
37+
/// This setting can be overridden by per-domain policies.
38+
@pragma('vm:entry-point')
39+
static bool _mayInsecurelyConnectToAllDomains = true;
40+
41+
/// Domain network policies set by embedder.
42+
@pragma('vm:entry-point')
43+
static void _setDomainPolicies(String domainNetworkPolicyJson) {
44+
_domainPolicies = _constructDomainPolicies(domainNetworkPolicyJson);
45+
}
46+
3547
// TODO(zra): Consider adding:
3648
// - an option to disallow modifying SecurityContext.defaultContext
3749
// - an option to disallow closing stdout and stderr.

sdk/lib/io/io.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ part 'io_sink.dart';
215215
part 'io_service.dart';
216216
part 'link.dart';
217217
part 'namespace_impl.dart';
218+
part 'network_policy.dart';
218219
part 'network_profiling.dart';
219220
part 'overrides.dart';
220221
part 'platform.dart';

sdk/lib/io/network_policy.dart

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
part of dart.io;
6+
7+
/// Whether insecure connections to [host] are allowed.
8+
///
9+
/// [host] must be a [String] or [InternetAddress].
10+
///
11+
/// If any of the domain policies match [host], the matching policy will make
12+
/// the decision. If multiple policies apply, the top matching policy makes the
13+
/// decision. If none of the domain policies match, the embedder default is
14+
/// used.
15+
///
16+
/// Loopback addresses are always allowed.
17+
bool isInsecureConnectionAllowed(dynamic host) {
18+
String hostString;
19+
if (host is String) {
20+
try {
21+
if ("localhost" == host || InternetAddress(host).isLoopback) return true;
22+
} on ArgumentError {
23+
// Assume not loopback.
24+
}
25+
hostString = host;
26+
} else if (host is InternetAddress) {
27+
if (host.isLoopback) return true;
28+
hostString = host.host;
29+
} else {
30+
throw ArgumentError.value(
31+
host, "host", "Must be a String or InternetAddress");
32+
}
33+
final topMatchedPolicy = _findBestDomainNetworkPolicy(hostString);
34+
final envOverride = bool.fromEnvironment(
35+
"dart.library.io.may_insecurely_connect_to_all_domains",
36+
defaultValue: true);
37+
return topMatchedPolicy?.allowInsecureConnections ??
38+
(envOverride && _EmbedderConfig._mayInsecurelyConnectToAllDomains);
39+
}
40+
41+
/// Policy for a specific domain.
42+
///
43+
/// [_DomainNetworkPolicy] can be used to create exceptions to the global
44+
/// network policy.
45+
class _DomainNetworkPolicy {
46+
/// https://tools.ietf.org/html/rfc1034#:~:text=Name%20space%20specifications
47+
///
48+
/// We specifically do not allow IP addresses.
49+
static final _domainMatcher = RegExp(
50+
r"^(?:[a-z\d-]{1,63}\.)+[a-z][a-z\d-]{0,62}$",
51+
caseSensitive: false);
52+
53+
/// The domain on which the policy is being set.
54+
///
55+
/// This cannot be a numeric IP address.
56+
///
57+
/// For example: `example.com`.
58+
final String domain;
59+
60+
/// Whether to allow insecure socket connections for this domain.
61+
final bool allowInsecureConnections;
62+
63+
/// Whether this domain policy covers sub-domains as well.
64+
///
65+
/// If this is true, all subdomains inherit the same policy. For instance,
66+
/// a policy set on `example.com` would apply to `*.example.com` such as
67+
/// `subdomain.example.com` or `www.example.com`.
68+
final bool includesSubDomains;
69+
70+
/// Creates a new domain exception in the network policy.
71+
///
72+
/// [domain] is the domain on which the policy is being set.
73+
///
74+
/// [includesSubDomains] determines whether the policy applies to
75+
/// all sub domains. If this is set to true, all subdomains inherit the
76+
/// same policy. For instance, a policy set on `example.com` would apply to
77+
/// `*.example.com` such as `subdomain.example.com` or `www.example.com`.
78+
///
79+
/// [allowInsecureConnections] determines whether to allow insecure socket
80+
/// connections for this [domain].
81+
_DomainNetworkPolicy(this.domain,
82+
{this.includesSubDomains = false,
83+
this.allowInsecureConnections = false}) {
84+
if (domain.length > 255 || !_domainMatcher.hasMatch(domain)) {
85+
throw ArgumentError.value(domain, "domain", "Invalid domain name");
86+
}
87+
}
88+
89+
/// Calculates how well the policy matches to a given host string.
90+
///
91+
/// A host matches a [policy] if it ends with its [domain].
92+
///
93+
/// A score is given to such a match depending on the specificity of the
94+
/// [domain]:
95+
///
96+
/// * A longer domain receives a higher score.
97+
/// * A domain that does not allow sub domains receives a higher score.
98+
///
99+
/// Returns -1 if the policy does not match.
100+
int matchScore(String host) {
101+
final domainLength = domain.length;
102+
final hostLength = host.length;
103+
final lengthDelta = hostLength - domainLength;
104+
if (host.endsWith(domain) &&
105+
(lengthDelta == 0 ||
106+
includesSubDomains && host.codeUnitAt(lengthDelta - 1) == 0x2e)) {
107+
return domainLength * 2 + (includesSubDomains ? 0 : 1);
108+
}
109+
return -1;
110+
}
111+
112+
/// Checks whether the [policy] to be added conflicts with existing policies.
113+
///
114+
/// Returns [true] if policy is safe to add to existing policy set and [false]
115+
/// if policy can safely be ignored.
116+
///
117+
/// Throws [ArgumentError] if a conflict is detected.
118+
bool checkConflict(List<_DomainNetworkPolicy> existingPolicies) {
119+
for (final existingPolicy in existingPolicies) {
120+
if (includesSubDomains == existingPolicy.includesSubDomains &&
121+
domain == existingPolicy.domain) {
122+
if (allowInsecureConnections ==
123+
existingPolicy.allowInsecureConnections) {
124+
// This is a duplicate policy
125+
return false;
126+
}
127+
throw StateError("Contradiction in the domain security policies: "
128+
"'$this' contradicts '$existingPolicy'");
129+
}
130+
}
131+
return true;
132+
}
133+
134+
/// This is used for encoding information about the policy in user visible
135+
/// errors.
136+
@override
137+
String toString() {
138+
final subDomainPrefix = includesSubDomains ? '*.' : '';
139+
final insecureConnectionPermission =
140+
allowInsecureConnections ? 'Allows' : 'Disallows';
141+
return "$subDomainPrefix$domain: "
142+
"$insecureConnectionPermission insecure connections";
143+
}
144+
}
145+
146+
/// Finds the top [DomainNetworkPolicy] instance that match given a single
147+
/// [domain].
148+
///
149+
/// We order the policies according to how specific they are. The final policy
150+
/// for a given [domain] is determined by the top matching
151+
/// [DomainNetworkPolicy].
152+
///
153+
/// Returns null if there's no matching policy.
154+
_DomainNetworkPolicy? _findBestDomainNetworkPolicy(String domain) {
155+
var topScore = 0;
156+
_DomainNetworkPolicy? topPolicy;
157+
for (final _DomainNetworkPolicy policy in _domainPolicies) {
158+
final score = policy.matchScore(domain);
159+
if (score > topScore) {
160+
topScore = score;
161+
topPolicy = policy;
162+
}
163+
}
164+
return topPolicy;
165+
}
166+
167+
/// Domain level policies that dart:io is enforcing.
168+
late List<_DomainNetworkPolicy> _domainPolicies =
169+
_constructDomainPolicies(null);
170+
171+
List<_DomainNetworkPolicy> _constructDomainPolicies(
172+
String? domainPoliciesString) {
173+
final domainPolicies = <_DomainNetworkPolicy>[];
174+
domainPoliciesString ??= String.fromEnvironment(
175+
"dart.library.io.domain_network_policies",
176+
defaultValue: "");
177+
if (domainPoliciesString.isNotEmpty) {
178+
final List<dynamic> policiesJson = json.decode(domainPoliciesString);
179+
for (final List<dynamic> policyJson in policiesJson) {
180+
assert(policyJson.length == 3);
181+
final policy = _DomainNetworkPolicy(
182+
policyJson[0],
183+
includesSubDomains: policyJson[1],
184+
allowInsecureConnections: policyJson[2],
185+
);
186+
if (policy.checkConflict(domainPolicies)) {
187+
domainPolicies.add(policy);
188+
}
189+
}
190+
}
191+
return domainPolicies;
192+
}

sdk/lib/io/socket.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,10 @@ abstract class Socket implements Stream<Uint8List>, IOSink {
801801
{sourceAddress, Duration? timeout}) {
802802
final IOOverrides? overrides = IOOverrides.current;
803803
if (overrides == null) {
804+
if (!isInsecureConnectionAllowed(host)) {
805+
throw new SocketException(
806+
"Insecure socket connections are disallowed by platform: $host");
807+
}
804808
return Socket._connect(host, port,
805809
sourceAddress: sourceAddress, timeout: timeout);
806810
}
@@ -815,6 +819,10 @@ abstract class Socket implements Stream<Uint8List>, IOSink {
815819
{sourceAddress}) {
816820
final IOOverrides? overrides = IOOverrides.current;
817821
if (overrides == null) {
822+
if (!isInsecureConnectionAllowed(host)) {
823+
throw new SocketException(
824+
"Insecure socket connections are disallowed by platform: $host");
825+
}
818826
return Socket._startConnect(host, port, sourceAddress: sourceAddress);
819827
}
820828
return overrides.socketStartConnect(host, port,

tests/standalone/io/http_ban_http_embedder_test.dart

Lines changed: 0 additions & 69 deletions
This file was deleted.

0 commit comments

Comments
 (0)