Repository navigation
[extension_discovery] Prevent config.yaml symlinks from escaping package boundary - #2556
Conversation
During extension discovery, maliciously crafted symlinks for config.yaml could allow an attacker to read and cache sensitive host files outside the package directory. This commit enforces that the fully resolved canonical path of config.yaml resides within the upstream package's canonical boundary, mitigating the path escape.
Package publishingIf you have publishing permissions, you can use the links below to publish the changes after merging this PR.
Documentation at https://github.com/dart-lang/ecosystem/wiki/Publishing-automation. |
There was a problem hiding this comment.
Code Review
This pull request introduces a security fix to prevent symlinks from causing config.yaml to be read from outside the package folder, mitigating potential symlink cache poisoning. It implements a boundary check using resolveSymbolicLinksSync() and adds corresponding tests. A review comment highlights that _isWithinPackageBoundary is called outside the try-catch block, which could cause findExtensions to crash if resolveSymbolicLinksSync() throws an IOException. It is recommended to move this check inside the try block to safely handle any exceptions.
PR Health
Unused Dependencies
|
| Package | Status |
|---|---|
| extension_discovery | ❗ Show IssuesThese packages are used outside lib/ but are not dev_dependencies: |
For details on how to fix these, see dependency_validator.
This check can be disabled by tagging the PR with skip-unused-dependencies-check.
Breaking changes ✔️
| Package | Change | Current Version | New Version | Needed Version | Looking good? |
|---|---|---|---|---|---|
| extension_discovery | Non-Breaking | 2.1.0 | 2.1.1-wip | 2.1.1-wip | ✔️ |
This check can be disabled by tagging the PR with skip-breaking-check.
API leaks ✔️
The following packages contain symbols visible in the public API, but not exported by the library. Export these symbols or remove them from your publicly visible API.
| Package | Leaked API symbol | Leaking sources |
|---|
This check can be disabled by tagging the PR with skip-leaking-check.
Changelog Entry ✔️
| Package | Changed Files |
|---|
Changes to files need to be accounted for in their respective changelogs.
This check can be disabled by tagging the PR with skip-changelog-check.
License Headers ✔️
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// 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.
| Files |
|---|
| no missing headers |
All source files should start with a license header.
Unrelated files missing license headers
| Files |
|---|
| pkgs/html/test/differential_test.dart |
| pkgs/html/test/dom_parsing_test.dart |
| pkgs/html/test/mxss_test.dart |
| pkgs/html/test/noah_ark_clause_limit_test.dart |
This check can be disabled by tagging the PR with skip-license-check.
Coverage ✔️
| File | Coverage |
|---|---|
| pkgs/extension_discovery/lib/extension_discovery.dart | 💚 59 % ⬆️ Infinity % |
This check for test coverage is informational (issues shown here will not fail the PR).
This check can be disabled by tagging the PR with skip-coverage-check.
| packageUri: p.packageUri, | ||
| config: parseYamlFromConfigFile(await configFile.readAsString()), | ||
| ); | ||
| if (_isWithinPackageBoundary(rootUri, configFile)) { |
There was a problem hiding this comment.
How about
if (configStat.isFileOrLink &&
_isWithinPackageBoundary(rootUri, configFile)) {There was a problem hiding this comment.
that would have been a bit more elegant :D
for the record it just landed via auto-merge while I was sleeping.
| if (configStat.isPossiblyModifiedAfter(registryStat.modified)) { | ||
| try { | ||
| registryUpdated = true; | ||
| registry[i] = ( | ||
| package: p.package, | ||
| rootUri: p.rootUri, | ||
| packageUri: p.packageUri, | ||
| config: parseYamlFromConfigFile(await configFile.readAsString()), | ||
| ); | ||
| continue; | ||
| } on FormatException { | ||
| // pass | ||
| } on IOException { | ||
| // pass | ||
| if (_isWithinPackageBoundary(rootUri, configFile)) { |
There was a problem hiding this comment.
How about
if (configStat.isPossiblyModifiedAfter(registryStat.modified) &&
_isWithinPackageBoundary(rootUri, configFile)) {| Uri fileUri(String path) => Uri.file(d.path(path)); | ||
| Uri directoryUri(String path) => Uri.directory(d.path(path)); | ||
|
|
||
| class _LinkDescriptor extends d.Descriptor { |
There was a problem hiding this comment.
Should we put this in package:test_descriptor? I imagine we'll need this in other places.
There was a problem hiding this comment.
If we want to expand package:test_descriptor we totally could, there is also a bunch of extensions to it in dart-lang/pub.
I don't want to block this PR on it.
And I fear that if I spend much time on test_descriptor I'll want to do bigger changes 🙈 🙈 🙈
Disallow
config.yamlfrom being a symlink pointing outside the package root.b/536195690