Skip to content

Fix Supabase config security for release builds: prevent asset fallback and config leaks#1

Merged
TNT-Likely merged 2 commits into
mainfrom
copilot/fix-41a13753-1b93-4641-8885-393b4cb1833e
Sep 9, 2025
Merged

Fix Supabase config security for release builds: prevent asset fallback and config leaks#1
TNT-Likely merged 2 commits into
mainfrom
copilot/fix-41a13753-1b93-4641-8885-393b4cb1833e

Conversation

Copilot AI commented Sep 9, 2025

Copy link
Copy Markdown

Problem

Release APK/AAB builds could not register or download because Supabase configuration was not being applied correctly at runtime. The code was designed to read from assets/config.json first, then apply compile-time defines (--dart-define) as overrides. This created two critical security issues:

  1. Config fallback risk: If CI secrets were missing/empty, release builds would fallback to development config from bundled assets
  2. Config leak risk: Development configuration was being bundled into release artifacts via assets/config.json

Solution

This PR implements a secure configuration strategy that ensures release builds only use CI-provided secrets while maintaining developer convenience for local builds.

Key Changes

🔒 Release Build Security (lib/config.dart)

  • Added kReleaseMode check to completely skip asset loading in release builds
  • Release builds now exclusively use --dart-define values from CI secrets
  • Debug/profile builds retain the convenience of loading from assets/config.json for local development
// Before: Always tried to load assets first
static Future<void> init() async {
  try {
    final txt = await rootBundle.loadString('assets/config.json');
    // ... load from assets
  } catch (_) {}
  // Then apply dart-define overrides
}

// After: Only load assets in non-release builds
static Future<void> init() async {
  if (!kReleaseMode) {
    try {
      final txt = await rootBundle.loadString('assets/config.json');
      // ... load from assets for development convenience
    } catch (_) {}
  }
  // Always apply dart-define values if present
}

📦 Asset Bundling Prevention (pubspec.yaml)

  • Removed assets/config.json from the assets list to prevent bundling in release builds
  • Eliminates risk of development config leaking into production artifacts
  • Developers can uncomment locally if needed for debugging

🛡️ CI Safeguards (.github/workflows/release.yml)

  • Added validation steps to ensure SUPABASE_URL and SUPABASE_ANON_KEY secrets exist before building
  • Added safe debug output showing secret lengths (not values) for troubleshooting
  • Provides fail-fast behavior if secrets are misconfigured

Behavior Matrix

Build Mode Assets Available Dart-Define Values Final Config Source
Release ❌ Not bundled ✅ From CI secrets CI secrets only
Debug ✅ Available ✅ From CI/local Assets + CI overrides
Debug ✅ Available ❌ Empty Assets fallback
Release ❌ Not bundled ❌ Empty Empty (CI should prevent)

API Compatibility

✅ All existing APIs remain unchanged:

  • AppConfig.supabaseUrl
  • AppConfig.supabaseAnonKey
  • AppConfig.hasSupabase
  • AppConfig.init()

Testing

The changes maintain backward compatibility while fixing the security issues. Release builds will now:

  1. Never attempt to read assets/config.json
  2. Never bundle development config into artifacts
  3. Fail fast during CI if secrets are missing
  4. Only use production configuration from --dart-define

This ensures that release artifacts can only connect to production Supabase instances with proper authentication, resolving the registration and download issues.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • canonical-lgw01.cdn.snapcraftcontent.com
  • https://storage.googleapis.com/flutter_infra_release/flutter/ddf47dd3ff96dbde6d9c614db0d7f019d7c7a2b7/dart-sdk-linux-x64.zip
    • Triggering command: curl --retry 3 --continue-at - --location --output /tmp/flutter/bin/cache/dart-sdk-linux-x64.zip REDACTED (http block)
  • https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.27.3-stable.tar.xz
    • Triggering command: curl -fsSL REDACTED (http block)

If you need me to access, download, or install something from one of these locations, you can either:

This pull request was created as a result of the following prompt from Copilot chat.

Context and problem

  • The release APK/AAB cannot register or download because Supabase service variables were not applied correctly at runtime. The code currently prefers reading Supabase config from assets/config.json, then overrides with compile-time defines (String.fromEnvironment via --dart-define) if present. During CI, --dart-define is passed from GitHub secrets.
  • Risk: If secrets are missing/empty or builds inadvertently rely on assets, release artifacts may end up using the asset config instead of CI-provided prod config. Additionally, bundling assets/config.json into release may leak development config and cause confusion.

Goal (Plan A)

  • In release builds, use only --dart-define values for Supabase config and DO NOT read from assets/config.json.
  • Do not package assets/config.json in release builds (remove it from pubspec assets list so it is not bundled).
  • Add CI safeguards: verify secrets exist and print safe lengths of injected values before building to ensure non-empty injection.

Scope of changes

  1. lib/config.dart
  • Import kReleaseMode and modify AppConfig.init():
    • Only attempt to load assets/config.json in non-release builds (for local development convenience), silently ignore failures.
    • Always let non-empty --dart-define values override any loaded values.
  • Keep the existing API surface (supabaseUrl, supabaseAnonKey, hasSupabase) the same.

Proposed patch for lib/config.dart:

import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter/foundation.dart' show kReleaseMode;

class AppConfig {
  // CI 构建时通过 --dart-define 注入
  static const _envUrl =
      String.fromEnvironment('SUPABASE_URL', defaultValue: '');
  static const _envAnon =
      String.fromEnvironment('SUPABASE_ANON_KEY', defaultValue: '');

  static String _supabaseUrl = '';
  static String _supabaseAnonKey = '';

  static String get supabaseUrl => _supabaseUrl;
  static String get supabaseAnonKey => _supabaseAnonKey;
  static bool get hasSupabase =>
      supabaseUrl.isNotEmpty && supabaseAnonKey.isNotEmpty;

  // 发布版仅使用 dart-define;非发布(调试/本地)可尝试从 assets 读取作为方便。
  static Future<void> init() async {
    // 非发布构建尝试读取 assets/config.json,失败则忽略
    if (!kReleaseMode) {
      try {
        final txt = await rootBundle.loadString('assets/config.json');
        final map = jsonDecode(txt) as Map<String, dynamic>;
        _supabaseUrl = (map['supabaseUrl'] as String?)?.trim() ?? '';
        _supabaseAnonKey = (map['supabaseAnonKey'] as String?)?.trim() ?? '';
      } catch (_) {
        // ignore, keep defaults and let env fill below
      }
    }

    // 若通过 --dart-define 提供了变量,则优先生效覆盖
    if (_envUrl.isNotEmpty) {
      _supabaseUrl = _envUrl;
    }
    if (_envAnon.isNotEmpty) {
      _supabaseAnonKey = _envAnon;
    }
  }
}
  1. pubspec.yaml
  • Remove assets/config.json from the assets list so it is not bundled into release artifacts. Keep other assets unchanged. This prevents accidental usage and leaking of config in release packages. Developers can re-enable it locally if desired.

Proposed patch for pubspec.yaml (only the flutter/assets section shown):

flutter:
  uses-material-design: true
  assets:
    # 发布产物不再打包 config.json,避免误用和泄露(本地如需调试,可临时添加)
    # - assets/config.json
    - assets/logo.svg
    - assets/logo.png
    - assets/title-logo.svg
  1. .github/workflows/release.yml
  • Add checks before builds to fail fast if secrets are missing.
  • Add a safe debug step to print the lengths of injected values for both Android and iOS jobs.
  • Keep the existing flutter build commands with --dart-define as-is.

Proposed insertions for .github/workflows/release.yml:

  • In the Android job, after "Flutter pub get":
      - name: Check Supabase secrets
        run: |
          test -n "${{ secrets.SUPABASE_URL }}" || (echo "Missing SUPABASE_URL" >&2; exit 1)
          test -n "${{ secrets.SUPABASE_ANON_KEY }}" || (echo "Missing SUPABASE_ANON_KEY" >&2; exit 1)

      - name: Print define length (debug, safe)
        run: |
          export SUPABASE_URL='${{ secrets.SUPABASE_URL }}'
          export SUPABASE_ANON_KEY='${{ secrets.SUPABASE_ANON_KEY }}'
          echo "SUPABASE_URL length: ${#SUPABASE_URL}"
          echo "SUPABASE_ANON_KEY length: ${#SUPABASE_ANON_KEY}"
  • In the iOS job, after "Flutter pub get":
      - name: Check Supabase secrets
        run: |
          test -n "${{ secrets.SUPABASE_URL }}" || (echo "Missing SUPABASE_URL" >&2; exit 1)
          test -n "${{ secrets.SUPABASE_ANON_KEY }}" || (echo "Missing SUPABASE_ANON_KEY" >&2; exit 1)

      - name: Print define length (debug, safe)
        run: |
          export SUPABASE_URL='${{ secrets.SUPABASE_URL }}'
          export SUPABASE_ANON_KEY='${{ secrets.SUPABASE_ANON_KEY }}'
          echo "SUPABASE_URL length: ${#SUPABASE_URL}"
          echo "SUPABASE_ANON_KEY length: ${#SUPABASE_ANON_KEY}"

Validation notes

  • After merging, trigger the workflow (tag push or manual). Check the new "Print define length" steps to ensure both lengths are > 0. The app will initialize Supabase only from dart-define in release builds.
  • If registration/download still fails, inspect runtime logs (adb logcat) for HTTP status and Supabase error messages to differentiate configuration vs. RLS/auth issues.

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

- Modified lib/config.dart to use kReleaseMode and only load assets in non-release builds
- Removed assets/config.json from pubspec.yaml to prevent bundling in release
- Added CI secret validation and debug steps to GitHub workflow

Co-authored-by: TNT-Likely <9278923+TNT-Likely@users.noreply.github.com>
Copilot AI changed the title [WIP] Release: use dart-define-only Supabase config; stop bundling assets/config.json; add CI secrets checks Fix Supabase config security for release builds: prevent asset fallback and config leaks Sep 9, 2025
Copilot AI requested a review from TNT-Likely September 9, 2025 16:26
@TNT-Likely TNT-Likely marked this pull request as ready for review September 9, 2025 16:31
@TNT-Likely TNT-Likely merged commit ffb3a7c into main Sep 9, 2025
@TNT-Likely TNT-Likely deleted the copilot/fix-41a13753-1b93-4641-8885-393b4cb1833e branch September 11, 2025 03:24
Robs87 pushed a commit to Robs87/BeeCount that referenced this pull request Jun 29, 2026
…ent/persistent error, tests)

Address all 4 review items from TNT-Likely#368:

- TNT-Likely#1 (blocking): Fix compile error - logger.warning() takes 3 params, not 4.
  Changed to logger.error() which accepts (tag, message, error, stackTrace).

- TNT-Likely#2 (blocking): Fix silent data loss in downgrade path.
  Added failedChangeIds to _PullPageOutcome. _runPullLoop now filters
  markResolved() to exclude failed changeIds, so UI can still see them.

- TNT-Likely#3: Distinguish transient vs persistent errors in downgrade path.
  Added _isTransientError() static method, reused by both
  _applyOneWithBusyRetry and downgrade path. Transient errors abort
  the page (blocked=true, cursor not advanced). Persistent errors
  are isolated and skipped.

- TNT-Likely#4: Added tests for downgrade path behavior, markResolved filtering,
  and _isTransientError logic.

Also: unified _isTransientError() usage in _applyOneWithBusyRetry.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants