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', () {