From ff830a08c153ba9020d4216bfd8d562c3ff3d2d5 Mon Sep 17 00:00:00 2001 From: VelikovPetar Date: Thu, 25 Jun 2026 12:27:20 +0200 Subject: [PATCH 1/3] fix(llc): Sanitize X-Stream-Client header --- .../stream_chat/lib/src/client/client.dart | 26 +++++-- .../core/http/system_environment_manager.dart | 53 ++++++++++--- .../stream_chat/lib/src/ws/websocket.dart | 2 +- .../http/system_environment_manager_test.dart | 75 ++++++++++++++++--- 4 files changed, 126 insertions(+), 30 deletions(-) diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 948cce2898..5ec363503f 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -166,21 +166,33 @@ class StreamChatClient { /// Updates the system environment information used by the client. /// - /// It allows you to set environment-specific information that will be - /// included in API requests, such as the application name, platform details, - /// and version information. + /// The passed [environment] is sanitized before being applied: + /// + /// Overridable fields (passed through as-is): + /// - [SystemEnvironment.appName] + /// - [SystemEnvironment.appVersion] + /// - [SystemEnvironment.osVersion] + /// - [SystemEnvironment.deviceModel] + /// + /// Immutable fields (custom values are ignored, internal defaults are + /// preserved): + /// - [SystemEnvironment.sdkName] + /// - [SystemEnvironment.sdkIdentifier] + /// - [SystemEnvironment.sdkVersion] + /// - [SystemEnvironment.osName] /// /// Example: /// ```dart /// client.updateSystemEnvironment( /// SystemEnvironment( - /// name: 'my_app', - /// version: '1.0.0', + /// sdkName: 'stream-chat', + /// sdkIdentifier: 'dart', + /// sdkVersion: StreamChatClient.packageVersion, + /// appName: 'my_app', + /// appVersion: '1.0.0', /// ), /// ); /// ``` - /// - /// See [SystemEnvironment] for more information on the available fields. void updateSystemEnvironment(SystemEnvironment environment) { _systemEnvironmentManager.updateEnvironment(environment); } diff --git a/packages/stream_chat/lib/src/core/http/system_environment_manager.dart b/packages/stream_chat/lib/src/core/http/system_environment_manager.dart index bb4648e7d3..8020b35128 100644 --- a/packages/stream_chat/lib/src/core/http/system_environment_manager.dart +++ b/packages/stream_chat/lib/src/core/http/system_environment_manager.dart @@ -11,15 +11,18 @@ class SystemEnvironmentManager { /// {@macro systemEnvironmentManager} SystemEnvironmentManager({ SystemEnvironment? environment, - }) : _environment = switch (environment) { - final env? => env, - _ => SystemEnvironment( - sdkName: 'stream-chat', - sdkIdentifier: 'dart', - sdkVersion: PACKAGE_VERSION, - osName: CurrentPlatform.name, - ), - }; + }) : _environment = SystemEnvironment( + sdkName: _sdkName, + sdkIdentifier: _sdkIdentifierDart, + sdkVersion: PACKAGE_VERSION, + osName: CurrentPlatform.name, + ) { + if (environment != null) updateEnvironment(environment); + } + + static const _sdkName = 'stream-chat'; + static const _sdkIdentifierDart = 'dart'; + static const _sdkIdentifierFlutter = 'flutter'; /// Returns the Stream client user agent string based on the current /// [environment] value. @@ -31,7 +34,37 @@ class SystemEnvironmentManager { /// Updates the current [SystemEnvironment]. void updateEnvironment(SystemEnvironment environment) { - _environment = environment; + _environment = _sanitize(environment); + } + + /// Sanitizes the passed [SystemEnvironment] + /// Ignores custom values for: + /// - sdkName + /// - sdkVersion + /// - osName + /// Allows only the dart -> flutter promotion for: + /// - sdkIdentifier (any other value, including a flutter -> dart + /// demotion, is ignored) + /// Allows overriding of: + /// - appName + /// - appVersion + /// - osVersion + /// - deviceModel + SystemEnvironment _sanitize(SystemEnvironment environment) { + final sdkIdentifier = environment.sdkIdentifier == _sdkIdentifierFlutter + ? _sdkIdentifierFlutter + : _environment.sdkIdentifier; + final osName = _environment.osName; + return SystemEnvironment( + sdkName: _sdkName, + sdkIdentifier: sdkIdentifier, + sdkVersion: PACKAGE_VERSION, + appName: environment.appName, + appVersion: environment.appVersion, + osName: osName, + osVersion: environment.osVersion, + deviceModel: environment.deviceModel, + ); } } diff --git a/packages/stream_chat/lib/src/ws/websocket.dart b/packages/stream_chat/lib/src/ws/websocket.dart index 280eeb8f51..127bc3361e 100644 --- a/packages/stream_chat/lib/src/ws/websocket.dart +++ b/packages/stream_chat/lib/src/ws/websocket.dart @@ -189,8 +189,8 @@ class WebSocket with TimerHelper { 'api_key': apiKey, 'authorization': token.rawValue, 'stream-auth-type': token.authType.name, - if (userAgent != null) 'X-Stream-Client': jsonEncode(userAgent), ...queryParameters, + if (userAgent != null) 'X-Stream-Client': jsonEncode(userAgent), }; final scheme = switch (baseUrl) { diff --git a/packages/stream_chat/test/src/core/http/system_environment_manager_test.dart b/packages/stream_chat/test/src/core/http/system_environment_manager_test.dart index 77980f0621..252f4a85c9 100644 --- a/packages/stream_chat/test/src/core/http/system_environment_manager_test.dart +++ b/packages/stream_chat/test/src/core/http/system_environment_manager_test.dart @@ -20,32 +20,83 @@ void main() { expect(manager.environment.sdkVersion, equals(PACKAGE_VERSION)); }); - test('initializes with custom environment', () { + test('sanitizes environment passed to constructor', () { const customEnv = SystemEnvironment( sdkName: 'custom-sdk', - sdkIdentifier: 'custom', + sdkIdentifier: 'flutter', sdkVersion: '1.0.0', + appName: 'test-app', ); manager = SystemEnvironmentManager(environment: customEnv); - expect(manager.environment.sdkName, equals('custom-sdk')); - expect(manager.environment.sdkIdentifier, equals('custom')); - expect(manager.environment.sdkVersion, equals('1.0.0')); + // Immutable fields are forced to internal values. + expect(manager.environment.sdkName, equals('stream-chat')); + expect(manager.environment.sdkVersion, equals(PACKAGE_VERSION)); + expect(manager.environment.osName, equals(CurrentPlatform.name)); + // Whitelisted sdkIdentifier is accepted. + expect(manager.environment.sdkIdentifier, equals('flutter')); + // App fields are passed through. + expect(manager.environment.appName, equals('test-app')); }); - test('updates environment', () { + test('sanitizes environment on update', () { const newEnv = SystemEnvironment( - sdkName: 'updated-sdk', - sdkIdentifier: 'updated', - sdkVersion: '2.0.0', + sdkName: 'stream-chat-android', + sdkIdentifier: 'android', + sdkVersion: '99.0.0', + appName: 'test-app', + appVersion: '2.0.0', + osName: 'spoofed-os', + osVersion: '14', + deviceModel: 'Pixel 7', ); manager.updateEnvironment(newEnv); - expect(manager.environment.sdkName, equals('updated-sdk')); - expect(manager.environment.sdkIdentifier, equals('updated')); - expect(manager.environment.sdkVersion, equals('2.0.0')); + // Immutable fields are forced to internal values. + expect(manager.environment.sdkName, equals('stream-chat')); + expect(manager.environment.sdkVersion, equals(PACKAGE_VERSION)); + expect(manager.environment.osName, equals(CurrentPlatform.name)); + // Unknown sdkIdentifier falls back to the current value. + expect(manager.environment.sdkIdentifier, equals('dart')); + // App/device/os-version fields are passed through. + expect(manager.environment.appName, equals('test-app')); + expect(manager.environment.appVersion, equals('2.0.0')); + expect(manager.environment.osVersion, equals('14')); + expect(manager.environment.deviceModel, equals('Pixel 7')); + }); + + test('allows promoting sdkIdentifier from dart to flutter', () { + manager.updateEnvironment( + const SystemEnvironment( + sdkName: 'stream-chat', + sdkIdentifier: 'flutter', + sdkVersion: PACKAGE_VERSION, + ), + ); + + expect(manager.environment.sdkIdentifier, equals('flutter')); + }); + + test('ignores demotion of sdkIdentifier from flutter to dart', () { + manager + ..updateEnvironment( + const SystemEnvironment( + sdkName: 'stream-chat', + sdkIdentifier: 'flutter', + sdkVersion: PACKAGE_VERSION, + ), + ) + ..updateEnvironment( + const SystemEnvironment( + sdkName: 'stream-chat', + sdkIdentifier: 'dart', + sdkVersion: PACKAGE_VERSION, + ), + ); + + expect(manager.environment.sdkIdentifier, equals('flutter')); }); test('userAgent returns proper header string', () { From 5f931f0f6be72e95384d22137cdb9b1e2e806284 Mon Sep 17 00:00:00 2001 From: VelikovPetar Date: Thu, 25 Jun 2026 14:48:40 +0200 Subject: [PATCH 2/3] refactor(llc): Extract SDK identifier promotion rule into extension type Replace the inline string comparison in `_sanitize` with a private `_SdkIdentifier` extension type that owns the known identifier constants and exposes a `precedence` getter. The promotion rule becomes a single precedence comparison and unknown identifiers cleanly fall back to the current value via the lowest precedence. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../core/http/system_environment_manager.dart | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/stream_chat/lib/src/core/http/system_environment_manager.dart b/packages/stream_chat/lib/src/core/http/system_environment_manager.dart index 8020b35128..be6a7ea51a 100644 --- a/packages/stream_chat/lib/src/core/http/system_environment_manager.dart +++ b/packages/stream_chat/lib/src/core/http/system_environment_manager.dart @@ -13,7 +13,7 @@ class SystemEnvironmentManager { SystemEnvironment? environment, }) : _environment = SystemEnvironment( sdkName: _sdkName, - sdkIdentifier: _sdkIdentifierDart, + sdkIdentifier: _SdkIdentifier.dart, sdkVersion: PACKAGE_VERSION, osName: CurrentPlatform.name, ) { @@ -21,8 +21,6 @@ class SystemEnvironmentManager { } static const _sdkName = 'stream-chat'; - static const _sdkIdentifierDart = 'dart'; - static const _sdkIdentifierFlutter = 'flutter'; /// Returns the Stream client user agent string based on the current /// [environment] value. @@ -51,9 +49,9 @@ class SystemEnvironmentManager { /// - osVersion /// - deviceModel SystemEnvironment _sanitize(SystemEnvironment environment) { - final sdkIdentifier = environment.sdkIdentifier == _sdkIdentifierFlutter - ? _sdkIdentifierFlutter - : _environment.sdkIdentifier; + final incoming = _SdkIdentifier(environment.sdkIdentifier); + final current = _SdkIdentifier(_environment.sdkIdentifier); + final sdkIdentifier = incoming.precedence < current.precedence ? current : incoming; final osName = _environment.osName; return SystemEnvironment( sdkName: _sdkName, @@ -96,3 +94,18 @@ extension XStreamClientHeaderExtension on SystemEnvironment { ].nonNulls.join('|'); } } + +/// Known SDK identifiers ranked by precedence. A proposed update is only +/// accepted when its precedence is greater than or equal to the current +/// identifier's, which makes the dart -> flutter transition one-way and +/// causes unknown identifiers to fall back to the current value. +extension type const _SdkIdentifier(String value) implements String { + static const dart = _SdkIdentifier('dart'); + static const flutter = _SdkIdentifier('flutter'); + + int get precedence => switch (this) { + dart => 0, + flutter => 1, + _ => -1, + }; +} From 81f6d7ec2c59e6e2056ab1851b9270f52e1006b4 Mon Sep 17 00:00:00 2001 From: VelikovPetar Date: Fri, 26 Jun 2026 16:41:00 +0200 Subject: [PATCH 3/3] fix(ui): Update chnagelog. --- packages/stream_chat/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 75445d8f25..4b938669e5 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +🔄 Changed + +- `StreamChatClient.updateSystemEnvironment` now sanitizes the passed `SystemEnvironment`: `sdkName`, `sdkVersion`, and `osName` are locked to internal defaults, and `sdkIdentifier` only accepts the `dart` → `flutter` promotion (other values, including a `flutter` → `dart` demotion, are ignored). `appName`, `appVersion`, `osVersion`, and `deviceModel` continue to pass through as-is. + ## 10.1.0 ✅ Added