From 43b9712fcd1b24225c9daff16f032b1dd65d5ea3 Mon Sep 17 00:00:00 2001 From: adilburaksen Date: Mon, 1 Jun 2026 13:26:51 +0300 Subject: [PATCH 1/2] Reject absolute paths in devToolsFileFromPath --- .../lib/src/server/file_system.dart | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/devtools_shared/lib/src/server/file_system.dart b/packages/devtools_shared/lib/src/server/file_system.dart index 58dc6bcf281..1f79c0fbdb4 100644 --- a/packages/devtools_shared/lib/src/server/file_system.dart +++ b/packages/devtools_shared/lib/src/server/file_system.dart @@ -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)) { // 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; } From a2e7371e8738843cd7eb0486ed45883761d6e14a Mon Sep 17 00:00:00 2001 From: adilburaksen Date: Mon, 1 Jun 2026 13:32:25 +0300 Subject: [PATCH 2/2] Add tests for devToolsFileFromPath path validation --- .../test/server/file_system_test.dart | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 packages/devtools_shared/test/server/file_system_test.dart diff --git a/packages/devtools_shared/test/server/file_system_test.dart b/packages/devtools_shared/test/server/file_system_test.dart new file mode 100644 index 00000000000..8baeb6a3513 --- /dev/null +++ b/packages/devtools_shared/test/server/file_system_test.dart @@ -0,0 +1,38 @@ +// 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( + '/home/user/.config/gcloud/application_default_credentials.json', + ), + isNull, + ); + }); + + test('rejects paths containing ".."', () { + expect(LocalFileSystem.devToolsFileFromPath('..'), isNull); + expect( + LocalFileSystem.devToolsFileFromPath('../../../etc/passwd'), + isNull, + ); + expect( + LocalFileSystem.devToolsFileFromPath('subdir/../../escape.json'), + isNull, + ); + }); + }); +}