fix(coverage): reject file: URIs outside known roots in Resolver (#2534)
diff --git a/pkgs/coverage/CHANGELOG.md b/pkgs/coverage/CHANGELOG.md
index 546b0bd..64c4183 100644
--- a/pkgs/coverage/CHANGELOG.md
+++ b/pkgs/coverage/CHANGELOG.md
@@ -6,6 +6,9 @@
completing and the resume request reaching the VM service.
- Also ignore `SentinelException` when resuming the main isolate after
collection, matching the existing `RPCError` handling there.
+- `Resolver.resolve` now rejects `file:` URIs that fall outside the known
+ package/SDK roots or the current directory, instead of resolving them
+ unconditionally.
## 1.15.1
diff --git a/pkgs/coverage/lib/src/resolver.dart b/pkgs/coverage/lib/src/resolver.dart
index 25e5c14..d43a932 100644
--- a/pkgs/coverage/lib/src/resolver.dart
+++ b/pkgs/coverage/lib/src/resolver.dart
@@ -89,7 +89,9 @@
return resolveSymbolicLinks(p.join(packagePath, pathInPackage));
}
if (uri.scheme == 'file') {
- return resolveSymbolicLinks(p.fromUri(uri));
+ final resolved = resolveSymbolicLinks(p.fromUri(uri));
+ if (resolved == null || !_isWithinKnownRoots(resolved)) return null;
+ return resolved;
}
// We cannot deal with anything else.
failed.add('$uri');
@@ -104,6 +106,30 @@
return File(normalizedPath).resolveSymbolicLinksSync();
}
+ /// The directories a `file:` URI is allowed to resolve into.
+ ///
+ /// `source` entries in coverage data are supplied by whatever produced the
+ /// coverage JSON, which this library treats as untrusted input: nothing
+ /// stops a crafted `file://` URI from pointing anywhere on disk, and
+ /// callers (`format_coverage --pretty-print`, `filterIgnored`) read the
+ /// resolved path's contents into their output. Without this check, a
+ /// coverage.json with `"source": "file:///etc/passwd"` gets that file's
+ /// contents printed straight into the coverage report.
+ List<String>? _knownRoots;
+
+ bool _isWithinKnownRoots(String path) {
+ final roots = _knownRoots ??= [
+ ?packagePath,
+ ?sdkRoot,
+ ...?_packages?.values.map(p.fromUri),
+ Directory.current.path,
+ ].map(p.normalize).toList();
+ final normalized = p.normalize(path);
+ return roots.any(
+ (root) => normalized == root || p.isWithin(root, normalized),
+ );
+ }
+
static Map<String, Uri> _parsePackages(String packagesPath) {
final content = File(packagesPath).readAsStringSync();
final packagesUri = p.toUri(packagesPath);
diff --git a/pkgs/coverage/test/resolver_test.dart b/pkgs/coverage/test/resolver_test.dart
index 708111a..16d7df1 100644
--- a/pkgs/coverage/test/resolver_test.dart
+++ b/pkgs/coverage/test/resolver_test.dart
@@ -2,6 +2,8 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
+import 'dart:io';
+
import 'package:coverage/src/resolver.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
@@ -131,6 +133,60 @@
);
expect(resolver.resolve('thing:foo/foo.dart'), null);
});
+
+ test('resolves file URIs within a known root', () async {
+ final resolver = await Resolver.create(
+ packagePath: p.join(d.sandbox, 'foo'),
+ );
+ final fooDartPath = p.join(d.sandbox, 'foo', 'lib', 'foo.dart');
+ expect(resolver.resolve(p.toUri(fooDartPath).toString()), fooDartPath);
+ });
+
+ test('resolves file URIs within sdkRoot', () async {
+ final resolver = await Resolver.create(
+ packagePath: p.join(d.sandbox, 'foo'),
+ sdkRoot: p.join(d.sandbox, 'sdk'),
+ );
+ final ioDartPath = p.join(d.sandbox, 'sdk', 'io', 'io.dart');
+ expect(resolver.resolve(p.toUri(ioDartPath).toString()), ioDartPath);
+ });
+
+ test('resolves file URIs within a known package root', () async {
+ final resolver = await Resolver.create(
+ packagesPath: p.join(
+ d.sandbox,
+ 'foo',
+ '.dart_tool',
+ 'package_config.json',
+ ),
+ );
+ final barDartPath = p.join(d.sandbox, 'bar', 'lib', 'bar.dart');
+ expect(resolver.resolve(p.toUri(barDartPath).toString()), barDartPath);
+ });
+
+ test('resolves file URIs within the current directory', () async {
+ final resolver = await Resolver.create();
+ final currentDirFile = File(
+ p.join(Directory.current.path, 'resolver_test_current_dir_probe.txt'),
+ )..writeAsStringSync('inside cwd');
+ addTearDown(currentDirFile.deleteSync);
+
+ expect(
+ resolver.resolve(p.toUri(currentDirFile.path).toString()),
+ currentDirFile.path,
+ );
+ });
+
+ test('does not resolve file URIs outside every known root', () async {
+ final resolver = await Resolver.create(
+ packagePath: p.join(d.sandbox, 'foo'),
+ sdkRoot: p.join(d.sandbox, 'sdk'),
+ );
+ final outsideFile = File(p.join(d.sandbox, 'outside.txt'))
+ ..writeAsStringSync('should not leak');
+
+ expect(resolver.resolve(p.toUri(outsideFile.path).toString()), null);
+ });
});
group('Bazel resolver', () {