From 5bf2e380264286b5b72f0e7a1c56e98c34f2d6f3 Mon Sep 17 00:00:00 2001 From: adilburaksen Date: Sat, 16 May 2026 21:57:16 +0300 Subject: [PATCH 1/3] fix: validate devtoolsOptionsUri scheme and filename in extensionEnabledState handleExtensionEnabledState() accepted the devtoolsOptionsUri query parameter without validation and passed it directly to DevToolsOptions.setExtensionEnabledState(), which creates the file and parent directories if they do not exist. An untrusted caller could supply an arbitrary file: URI to create or overwrite any path writable by the DevTools server process. Add a guard that rejects URIs whose scheme is not 'file' or whose path does not end with 'devtools_options.yaml'. Both conditions must hold for a legitimate devtools options file. --- .../src/server/handlers/_devtools_extensions.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/devtools_shared/lib/src/server/handlers/_devtools_extensions.dart b/packages/devtools_shared/lib/src/server/handlers/_devtools_extensions.dart index f7c63671630..324fd30e8c4 100644 --- a/packages/devtools_shared/lib/src/server/handlers/_devtools_extensions.dart +++ b/packages/devtools_shared/lib/src/server/handlers/_devtools_extensions.dart @@ -80,6 +80,19 @@ extension _ExtensionsApiHandler on Never { final devtoolsOptionsFileUriString = queryParams[ExtensionsApi.devtoolsOptionsUriPropertyName]!; final devtoolsOptionsFileUri = Uri.parse(devtoolsOptionsFileUriString); + + // Validate that the URI is a local file URI pointing to a + // 'devtools_options.yaml' file. Accepting arbitrary URIs from the query + // string would allow an untrusted caller to create or overwrite any file + // writable by the DevTools server process. + if (devtoolsOptionsFileUri.scheme != 'file' || + !devtoolsOptionsFileUri.path.endsWith('devtools_options.yaml')) { + return api.badRequest( + 'Invalid devtoolsOptionsUri: must be a file: URI ending in ' + "'devtools_options.yaml'.", + ); + } + final extensionName = queryParams[ExtensionsApi.extensionNamePropertyName]!; final activate = queryParams[ExtensionsApi.enabledStatePropertyName]; From 1fe700ff955a17f7d2ae93936e0d89186ec838cf Mon Sep 17 00:00:00 2001 From: adilburaksen Date: Sat, 16 May 2026 22:10:12 +0300 Subject: [PATCH 2/3] fix: restrict devtools_options.yaml path check to exact filename endsWith('devtools_options.yaml') matched paths like 'not_devtools_options.yaml'. Adding the path separator prefix ensures only files literally named 'devtools_options.yaml' are accepted. --- .../lib/src/server/handlers/_devtools_extensions.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devtools_shared/lib/src/server/handlers/_devtools_extensions.dart b/packages/devtools_shared/lib/src/server/handlers/_devtools_extensions.dart index 324fd30e8c4..b5b10bbd8a6 100644 --- a/packages/devtools_shared/lib/src/server/handlers/_devtools_extensions.dart +++ b/packages/devtools_shared/lib/src/server/handlers/_devtools_extensions.dart @@ -86,7 +86,7 @@ extension _ExtensionsApiHandler on Never { // string would allow an untrusted caller to create or overwrite any file // writable by the DevTools server process. if (devtoolsOptionsFileUri.scheme != 'file' || - !devtoolsOptionsFileUri.path.endsWith('devtools_options.yaml')) { + !devtoolsOptionsFileUri.path.endsWith('/devtools_options.yaml')) { return api.badRequest( 'Invalid devtoolsOptionsUri: must be a file: URI ending in ' "'devtools_options.yaml'.", From a9e77d16bc0eb69f549e6279b8d775b3ebdc5e39 Mon Sep 17 00:00:00 2001 From: adilburaksen Date: Fri, 5 Jun 2026 23:53:47 +0300 Subject: [PATCH 3/3] Harden devtoolsOptionsUri validation for Windows file URIs Resolve the file name via Uri.toFilePath() + p.basename instead of a forward-slash string match so the check holds for Windows file URIs (both '/' and '\' separators) and reject non-local (UNC) authorities. Bump devtools_shared to 13.0.2 and add a changelog entry. --- packages/devtools_shared/CHANGELOG.md | 5 +++++ .../server/handlers/_devtools_extensions.dart | 22 +++++++++++++------ .../lib/src/server/server_api.dart | 1 + packages/devtools_shared/pubspec.yaml | 2 +- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/devtools_shared/CHANGELOG.md b/packages/devtools_shared/CHANGELOG.md index 0f1f770d1a9..f6fa2bc2db5 100644 --- a/packages/devtools_shared/CHANGELOG.md +++ b/packages/devtools_shared/CHANGELOG.md @@ -3,6 +3,11 @@ Copyright 2025 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. --> +# 13.0.2 +* Validate the `devtoolsOptionsUri` query parameter in the extension enabled + state handler so it must be a `file:` URI named `devtools_options.yaml`, + preventing arbitrary file writes by the DevTools server process. + # 13.0.1 * Handle null values for `FlutterStore.flutterClientId`. diff --git a/packages/devtools_shared/lib/src/server/handlers/_devtools_extensions.dart b/packages/devtools_shared/lib/src/server/handlers/_devtools_extensions.dart index b5b10bbd8a6..e87e2704d23 100644 --- a/packages/devtools_shared/lib/src/server/handlers/_devtools_extensions.dart +++ b/packages/devtools_shared/lib/src/server/handlers/_devtools_extensions.dart @@ -81,14 +81,22 @@ extension _ExtensionsApiHandler on Never { queryParams[ExtensionsApi.devtoolsOptionsUriPropertyName]!; final devtoolsOptionsFileUri = Uri.parse(devtoolsOptionsFileUriString); - // Validate that the URI is a local file URI pointing to a - // 'devtools_options.yaml' file. Accepting arbitrary URIs from the query - // string would allow an untrusted caller to create or overwrite any file - // writable by the DevTools server process. - if (devtoolsOptionsFileUri.scheme != 'file' || - !devtoolsOptionsFileUri.path.endsWith('/devtools_options.yaml')) { + // Validate that the URI is a local file URI whose file name is exactly + // 'devtools_options.yaml'. Accepting arbitrary URIs from the query string + // would allow an untrusted caller to create or overwrite any file writable + // by the DevTools server process. Resolving the name through + // `Uri.toFilePath()` + `p.basename` handles both '/' and '\' path + // separators, so the check holds for Windows file URIs as well. Requiring + // an empty host rejects UNC paths (e.g. `file://server/share/...`) and + // keeps `toFilePath()` from throwing on a non-local authority. + final isFileUri = devtoolsOptionsFileUri.scheme == 'file' && + devtoolsOptionsFileUri.host.isEmpty; + final fileName = isFileUri + ? p.basename(devtoolsOptionsFileUri.toFilePath()) + : ''; + if (!isFileUri || fileName != 'devtools_options.yaml') { return api.badRequest( - 'Invalid devtoolsOptionsUri: must be a file: URI ending in ' + 'Invalid devtoolsOptionsUri: must be a file: URI named ' "'devtools_options.yaml'.", ); } diff --git a/packages/devtools_shared/lib/src/server/server_api.dart b/packages/devtools_shared/lib/src/server/server_api.dart index f9b50724845..cc41fb0d9e7 100644 --- a/packages/devtools_shared/lib/src/server/server_api.dart +++ b/packages/devtools_shared/lib/src/server/server_api.dart @@ -11,6 +11,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:dtd/dtd.dart'; import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; import 'package:shelf/shelf.dart' as shelf; import 'package:vm_service/vm_service.dart'; diff --git a/packages/devtools_shared/pubspec.yaml b/packages/devtools_shared/pubspec.yaml index dce148c2109..585ad854798 100644 --- a/packages/devtools_shared/pubspec.yaml +++ b/packages/devtools_shared/pubspec.yaml @@ -4,7 +4,7 @@ name: devtools_shared description: Package of shared Dart structures between devtools_app, dds, and other tools. -version: 13.0.1 +version: 13.0.2 repository: https://github.com/flutter/devtools/tree/master/packages/devtools_shared