Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ To learn more about DevTools, check out the
* Fixed a bug where highlighted search matches in tables were unreadable in dark
mode because the highlight color had become fully opaque. -
[#9863](https://github.com/flutter/devtools/pull/9863)
* Rejected absolute paths in DevTools server file reads so they stay within
the `~/.flutter-devtools/` directory and cannot resolve to arbitrary files
on disk. -
[#9844](https://github.com/flutter/devtools/pull/9844)

## Inspector updates

Expand Down
16 changes: 13 additions & 3 deletions packages/devtools_shared/lib/src/server/file_system.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,24 @@ extension LocalFileSystem on Never {
///
/// Only files within ~/.flutter-devtools/ can be accessed.
static File? devToolsFileFromPath(String pathFromDevToolsDir) {
if (pathFromDevToolsDir.contains('..')) {
if (pathFromDevToolsDir.contains('..') ||
path.isAbsolute(pathFromDevToolsDir)) {
Comment thread
srawlins marked this conversation as resolved.
// The passed in path should not be able to walk up the directory tree
// outside of the ~/.flutter-devtools/ directory.
// outside of the ~/.flutter-devtools/ directory. It must also not be an
// absolute path: path.join() discards the base directory when its second
// argument is absolute, which would otherwise allow reading an arbitrary
// file on disk (e.g. an absolute path to a credentials .json file).
return null;
}

ensureDevToolsDirectory();
final file = File(path.join(devToolsDir(), pathFromDevToolsDir));
final devToolsDirPath = devToolsDir();
final file = File(path.join(devToolsDirPath, pathFromDevToolsDir));
// Defense in depth: ensure the resolved path is actually contained within
// the DevTools directory.
if (!path.isWithin(devToolsDirPath, file.path)) {
return null;
}
if (!file.existsSync()) {
return null;
}
Expand Down
36 changes: 36 additions & 0 deletions packages/devtools_shared/test/server/file_system_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2026 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

import 'package:devtools_shared/src/server/file_system.dart';
import 'package:test/test.dart';

void main() {
group('LocalFileSystem.devToolsFileFromPath path validation', () {
// These inputs must be rejected before any filesystem access so that reads
// stay confined to the ~/.flutter-devtools/ directory.

test('rejects absolute paths', () {
// path.join() discards the base directory when its second argument is
// absolute, so an absolute path would otherwise escape the DevTools
// directory and read an arbitrary file on disk.
expect(LocalFileSystem.devToolsFileFromPath('/etc/passwd'), isNull);
expect(
LocalFileSystem.devToolsFileFromPath('/absolute/path/to/file.json'),
isNull,
);
});

test('rejects paths containing ".."', () {
expect(LocalFileSystem.devToolsFileFromPath('..'), isNull);
expect(
LocalFileSystem.devToolsFileFromPath('../../../etc/passwd'),
isNull,
);
expect(
LocalFileSystem.devToolsFileFromPath('subdir/../../escape.json'),
isNull,
);
});
});
}
Loading