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
22 changes: 19 additions & 3 deletions packages/supabase_flutter/lib/src/local_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,38 @@ class EmptyLocalStorage extends LocalStorage {
Future<void> persistSession(persistSessionString) async {}
}

/// A [LocalStorage] implementation that implements SharedPreferences as the
/// A [LocalStorage] implementation that implements SharedPreferencesAsync as the
/// storage method.
class SharedPreferencesLocalStorage extends LocalStorage {
late final SharedPreferences _prefs;
late final SharedPreferencesAsync _prefs;

SharedPreferencesLocalStorage({required this.persistSessionKey});

final String persistSessionKey;

static const _useWebLocalStorage =
kIsWeb && bool.fromEnvironment("dart.library.js_interop");

@override
Future<void> initialize() async {
if (!_useWebLocalStorage) {
WidgetsFlutterBinding.ensureInitialized();
_prefs = await SharedPreferences.getInstance();
_prefs = SharedPreferencesAsync();

await _maybeMigrateAccessToken();
}
}

Future<void> _maybeMigrateAccessToken() async {
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new migration logic in _maybeMigrateAccessToken lacks test coverage. Since the existing tests for SharedPreferencesLocalStorage use setMockInitialValues which may not properly test the migration from legacy SharedPreferences to SharedPreferencesAsync, add tests to verify: 1) successful migration when legacy data exists, 2) no-op when legacy data doesn't exist, and 3) proper cleanup of legacy storage after migration.

Copilot uses AI. Check for mistakes.
final legacyPrefs = await SharedPreferences.getInstance();

if (legacyPrefs.containsKey(persistSessionKey)) {
final accessToken = legacyPrefs.getString(persistSessionKey);

if (accessToken != null) {
await legacyPrefs.remove(persistSessionKey);
await _prefs.setString(persistSessionKey, accessToken);
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable accessToken appears to hold a Supabase auth/session token and is being stored in cleartext using SharedPreferencesAsync, which typically persists data in an unencrypted file on the device. Because this is a bearer token, any attacker or malicious app able to read the underlying shared preferences storage (e.g., via device compromise, backup extraction, or sandbox escape) can reuse this token to impersonate the user against Supabase, leading to account compromise. Severity: HIGH. Confidence: 8

Copilot uses AI. Check for mistakes.
}
Comment on lines +87 to +95
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration logic should handle errors gracefully. If the migration fails (e.g., due to storage access issues), the error could leave the session in an inconsistent state or prevent initialization. Consider wrapping the migration in a try-catch block and logging any errors, allowing initialization to proceed even if migration fails.

Suggested change
final legacyPrefs = await SharedPreferences.getInstance();
if (legacyPrefs.containsKey(persistSessionKey)) {
final accessToken = legacyPrefs.getString(persistSessionKey);
if (accessToken != null) {
await legacyPrefs.remove(persistSessionKey);
await _prefs.setString(persistSessionKey, accessToken);
}
try {
final legacyPrefs = await SharedPreferences.getInstance();
if (legacyPrefs.containsKey(persistSessionKey)) {
final accessToken = legacyPrefs.getString(persistSessionKey);
if (accessToken != null) {
await legacyPrefs.remove(persistSessionKey);
await _prefs.setString(persistSessionKey, accessToken);
}
}
} catch (e, stackTrace) {
debugPrint(
'Error while migrating access token for key "$persistSessionKey": $e');
debugPrint(stackTrace.toString());

Copilot uses AI. Check for mistakes.
}
}

Expand Down
6 changes: 3 additions & 3 deletions packages/supabase_flutter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ repository: 'https://github.com/supabase/supabase-flutter/tree/main/packages/sup
documentation: 'https://supabase.com/docs/reference/dart/introduction'

environment:
sdk: '>=3.3.0 <4.0.0'
flutter: '>=3.19.0'
sdk: '>=3.4.0 <4.0.0'
flutter: '>=3.22.0'
Comment on lines +9 to +10
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SDK and Flutter version bumps are significant breaking changes (from 3.3.0 to 3.4.0 and 3.19.0 to 3.22.0). These version requirements should be justified - are they necessary for SharedPreferencesAsync support? If not, consider using the original minimum versions to avoid unnecessarily restricting the user base.

Suggested change
sdk: '>=3.4.0 <4.0.0'
flutter: '>=3.22.0'
sdk: '>=3.3.0 <4.0.0'
flutter: '>=3.19.0'

Copilot uses AI. Check for mistakes.

dependencies:
app_links: '>=6.2.0 <8.0.0'
Expand All @@ -20,7 +20,7 @@ dependencies:
supabase: 2.10.2
url_launcher: ^6.1.2
path_provider: ^2.0.0
shared_preferences: ^2.0.0
shared_preferences: ^2.3.0
logging: ^1.2.0
web: '>=0.5.0 <2.0.0'

Expand Down
Loading