[{"data":1,"prerenderedAt":28},["ShallowReactive",2],{"articolo-sicurezza-delle-app-flutter-offuscamento-certificate-pinning-e-gestione-dei-segreti":3,"comments-article-sicurezza-delle-app-flutter-offuscamento-certificate-pinning-e-gestione-dei-segreti":27},{"id":4,"title":5,"slug":6,"excerpt":7,"body":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"status":16,"published_at":17,"meta_title":18,"meta_description":19,"category":20,"author":24},87,"Sicurezza delle app Flutter: offuscamento, certificate pinning e gestione dei segreti","sicurezza-delle-app-flutter-offuscamento-certificate-pinning-e-gestione-dei-segreti","Una guida pratica per irrobustire le app Flutter in produzione: offuscamento del codice Dart, symbolication dei crash, certificate pinning con Dio, gestione corretta delle API key e protezione dello schermo.","## Perché la sicurezza di un'app Flutter va progettata, non improvvisata\n\nUn binario Flutter compilato in AOT non è \"leggibile\" come un bundle JavaScript, ma non è nemmeno una cassaforte. Con strumenti open source è possibile estrarre stringhe, individuare gli endpoint delle API, leggere chiavi hardcoded e ispezionare il traffico di rete con un proxy come mitmproxy o Charles.\n\nIn questo articolo vediamo le contromisure concrete che ogni app Flutter destinata alla produzione dovrebbe adottare: offuscamento del codice, certificate pinning, gestione dei segreti tramite build-time configuration, hardening della piattaforma nativa e igiene del logging.\n\nUna premessa che vale per tutto ciò che segue: **niente di ciò che gira sul dispositivo dell'utente è realmente segreto**. L'obiettivo non è rendere l'attacco impossibile, ma renderlo abbastanza costoso da scoraggiarlo, spostando i controlli critici sul backend.\n\n## 1. Offuscamento del codice Dart\n\nDi default, il codice AOT conserva i nomi di classi, metodi e funzioni. Estrarre i simboli da un APK e ricostruire la logica applicativa è quindi relativamente semplice. Flutter offre l'offuscamento nativo:\n\n```bash\n# Android\nflutter build appbundle --release \\\n  --obfuscate \\\n  --split-debug-info=build\u002Fsymbols\u002Fandroid\n\n# iOS\nflutter build ipa --release \\\n  --obfuscate \\\n  --split-debug-info=build\u002Fsymbols\u002Fios\n```\n\n`--obfuscate` rinomina i simboli, mentre `--split-debug-info` estrae la mappa dei simboli in file esterni. **I due flag vanno usati sempre insieme**: senza la mappa, gli stack trace dei crash diventano illeggibili.\n\n### Conservare e usare i simboli\n\nI file `.symbols` prodotti nella cartella indicata sono l'equivalente delle dSYM di iOS: vanno archiviati come artefatto della pipeline CI, versione per versione, e **mai** committati nel repository dell'app.\n\nPer rendere leggibile uno stack trace offuscato:\n\n```bash\nflutter symbolize \\\n  -i crash_stacktrace.txt \\\n  -d build\u002Fsymbols\u002Fandroid\u002Fapp.android-arm64.symbols\n```\n\nSe usi Sentry o Firebase Crashlytics, esistono plugin che caricano automaticamente i simboli durante la build, ottenendo la deoffuscazione trasparente nella dashboard.\n\n### Attenzione a reflection e serializzazione\n\nL'offuscamento può rompere codice che si basa sui nomi dei tipi a runtime, ad esempio `runtimeType.toString()` usato come chiave. Se hai logiche di questo tipo, sostituiscile con enum o costanti esplicite:\n\n```dart\n\u002F\u002F ❌ Fragile con --obfuscate\nfinal key = model.runtimeType.toString();\n\n\u002F\u002F ✅ Stabile\nabstract interface class Identifiable {\n  String get typeKey;\n}\n\nclass UserModel implements Identifiable {\n  @override\n  String get typeKey => 'user';\n}\n```\n\n## 2. Gestione dei segreti: `--dart-define-from-file`\n\nUn errore ricorrente è inserire chiavi e URL direttamente nel codice o, peggio, in un file `.dart` committato. La soluzione idiomatica è passare la configurazione a build time.\n\nCrea un file `env\u002Fprod.json` (aggiunto a `.gitignore`):\n\n```json\n{\n  \"API_BASE_URL\": \"https:\u002F\u002Fapi.example.com\",\n  \"SENTRY_DSN\": \"https:\u002F\u002Fxxx@o0.ingest.sentry.io\u002F0\",\n  \"ENABLE_LOGS\": false\n}\n```\n\nE lancia la build così:\n\n```bash\nflutter build appbundle --release \\\n  --dart-define-from-file=env\u002Fprod.json \\\n  --obfuscate --split-debug-info=build\u002Fsymbols\u002Fandroid\n```\n\nNel codice, i valori si leggono con `String.fromEnvironment` in contesto `const`:\n\n```dart\nabstract final class Env {\n  static const apiBaseUrl = String.fromEnvironment(\n    'API_BASE_URL',\n    defaultValue: 'https:\u002F\u002Fapi.staging.example.com',\n  );\n\n  static const sentryDsn = String.fromEnvironment('SENTRY_DSN');\n\n  static const enableLogs = bool.fromEnvironment('ENABLE_LOGS');\n\n  static void assertValid() {\n    assert(apiBaseUrl.isNotEmpty, 'API_BASE_URL non configurato');\n  }\n}\n```\n\n> **Importante**: `--dart-define` non cripta nulla. I valori finiscono comunque nel binario e sono estraibili. Serve a evitare che i segreti finiscano nel version control e a differenziare gli ambienti, non a proteggerli. Chiavi realmente sensibili (secret di pagamento, credenziali di servizi terzi con costi a consumo) devono stare **solo sul backend**, esposte tramite un endpoint autenticato.\n\nPer i token utente (access token, refresh token) usa invece lo storage cifrato del sistema operativo, non `SharedPreferences`.\n\n## 3. Certificate pinning con Dio\n\nIl pinning impedisce a un proxy con certificato installato dall'utente (o a una CA compromessa) di intercettare il traffico. Con Dio 5.x e l'adapter IO si può implementare in modo compatto validando l'impronta SHA-256 del certificato.\n\n```dart\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:crypto\u002Fcrypto.dart';\nimport 'package:dio\u002Fdio.dart';\nimport 'package:dio\u002Fio.dart';\n\n\u002F\u002F\u002F Impronte SHA-256 dei certificati accettati.\n\u002F\u002F\u002F Includi sempre almeno un pin di backup per la rotazione.\nconst _pinnedFingerprints = \u003CString>{\n  'YLh1dUR9y6Kja30RrAn7JKnbQG\u002FuEtLMkBgFF2Fuihg=',\n  'Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys=',\n};\n\nDio buildPinnedDio() {\n  final dio = Dio(BaseOptions(baseUrl: Env.apiBaseUrl));\n\n  dio.httpClientAdapter = IOHttpClientAdapter(\n    createHttpClient: () {\n      final client = HttpClient();\n      \u002F\u002F Nessuna eccezione sui certificati non validi.\n      client.badCertificateCallback = (cert, host, port) => false;\n      return client;\n    },\n    validateCertificate: (cert, host, port) {\n      if (cert == null) return false;\n      final fingerprint = base64.encode(sha256.convert(cert.der).bytes);\n      return _pinnedFingerprints.contains(fingerprint);\n    },\n  );\n\n  return dio;\n}\n```\n\nPer ricavare l'impronta del certificato del tuo dominio:\n\n```bash\nopenssl s_client -connect api.example.com:443 -servername api.example.com \\\n  \u003C \u002Fdev\u002Fnull 2>\u002Fdev\u002Fnull \\\n  | openssl x509 -outform DER \\\n  | openssl dgst -sha256 -binary \\\n  | base64\n```\n\n### Regole d'oro del pinning\n\n- **Sempre almeno due pin**: quello attivo e quello del prossimo certificato. Senza pin di backup, alla scadenza del certificato tutte le app installate smettono di funzionare e servirà un aggiornamento dagli store.\n- **Meglio pinnare la CA intermedia** che il certificato foglia, se il tuo provider ruota spesso (es. Let's Encrypt ogni 90 giorni).\n- **Prevedi un kill switch remoto**: un flag su Remote Config o un endpoint di configurazione che permetta di disabilitare il pinning in caso di emergenza.\n- Il pinning non si applica al traffico gestito da WebView o da SDK di terze parti: valutalo caso per caso.\n\n## 4. Hardening lato piattaforma\n\n### Android\n\nBlocca il traffico in chiaro e limita le CA attendibili con un `network_security_config.xml` in `android\u002Fapp\u002Fsrc\u002Fmain\u002Fres\u002Fxml\u002F`:\n\n```xml\n\u003C?xml version=\"1.0\" encoding=\"utf-8\"?>\n\u003Cnetwork-security-config>\n  \u003Cbase-config cleartextTrafficPermitted=\"false\">\n    \u003Ctrust-anchors>\n      \u003Ccertificates src=\"system\" \u002F>\n    \u003C\u002Ftrust-anchors>\n  \u003C\u002Fbase-config>\n\u003C\u002Fnetwork-security-config>\n```\n\nOmettere `\u003Ccertificates src=\"user\" \u002F>` significa ignorare i certificati installati manualmente dall'utente: già da solo blocca la maggior parte dei proxy di intercettazione. Referenzialo nel manifest:\n\n```xml\n\u003Capplication\n    android:networkSecurityConfig=\"@xml\u002Fnetwork_security_config\"\n    android:allowBackup=\"false\"\n    ... >\n```\n\n`android:allowBackup=\"false\"` evita che i dati dell'app finiscano nei backup ADB o cloud, un vettore classico per l'estrazione di token.\n\nAbilita inoltre R8 per la parte nativa nel `build.gradle` del modulo app:\n\n```groovy\nbuildTypes {\n    release {\n        minifyEnabled true\n        shrinkResources true\n        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),\n                      'proguard-rules.pro'\n    }\n}\n```\n\n### iOS\n\nVerifica che App Transport Security non sia stato allentato durante lo sviluppo: nel file `Info.plist` **non** devono comparire `NSAllowsArbitraryLoads` a `true` né eccezioni per domini di produzione.\n\n## 5. Proteggere lo schermo e il multitasking\n\nPer app bancarie, sanitarie o che mostrano documenti, è buona pratica impedire screenshot e nascondere il contenuto nell'app switcher. Pacchetti come `screen_protector` o `no_screenshot` incapsulano `FLAG_SECURE` su Android e l'oscuramento della finestra su iOS:\n\n```dart\nimport 'package:screen_protector\u002Fscreen_protector.dart';\n\nclass SecureScreen extends StatefulWidget {\n  const SecureScreen({super.key, required this.child});\n  final Widget child;\n\n  @override\n  State\u003CSecureScreen> createState() => _SecureScreenState();\n}\n\nclass _SecureScreenState extends State\u003CSecureScreen> {\n  @override\n  void initState() {\n    super.initState();\n    ScreenProtector.preventScreenshotOn();\n    ScreenProtector.protectDataLeakageWithBlur();\n  }\n\n  @override\n  void dispose() {\n    ScreenProtector.preventScreenshotOff();\n    ScreenProtector.protectDataLeakageWithBlurOff();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) => widget.child;\n}\n```\n\nApplicalo solo alle schermate sensibili: disabilitare gli screenshot ovunque è una scelta che penalizza l'esperienza utente.\n\n## 6. Igiene del logging\n\nI log sono una delle fonti di leak più sottovalutate: su Android sono leggibili via `adb logcat` anche in release. Centralizza il logging e disattivalo in produzione:\n\n```dart\nimport 'package:flutter\u002Ffoundation.dart';\n\nabstract final class AppLogger {\n  static void debug(String message) {\n    if (kReleaseMode) return;\n    debugPrint('[DEBUG] $message');\n  }\n\n  \u002F\u002F\u002F Da usare solo per messaggi già sanificati.\n  static void info(String message) => debugPrint('[INFO] $message');\n}\n```\n\nE se usi gli interceptor di Dio per il logging, ricordati di filtrare gli header sensibili:\n\n```dart\nif (!kReleaseMode) {\n  dio.interceptors.add(\n    LogInterceptor(\n      requestHeader: false, \u002F\u002F evita di stampare Authorization\n      responseBody: true,\n    ),\n  );\n}\n```\n\nLo stesso vale per i breadcrumb inviati ai servizi di crash reporting: configura sempre una funzione di scrubbing che rimuova token, email e numeri di carta prima dell'invio.\n\n## 7. Rilevare ambienti compromessi\n\nSu dispositivi con root o jailbreak le protezioni del sistema operativo (keystore, sandbox) sono aggirabili. Pacchetti come `flutter_jailbreak_detection` permettono di reagire:\n\n```dart\nFuture\u003Cbool> isDeviceTrusted() async {\n  if (kDebugMode) return true;\n  final jailbroken = await FlutterJailbreakDetection.jailbroken;\n  final developerMode = await FlutterJailbreakDetection.developerMode;\n  return !jailbroken && !developerMode;\n}\n```\n\nÈ un controllo aggirabile, quindi non va usato come unica barriera: ha senso come segnale aggiuntivo da inviare al backend, che può applicare policy più restrittive (es. limiti di importo, richiesta di step-up authentication).\n\nPer un livello superiore esistono le API di **attestation** (Play Integrity su Android, App Attest su iOS), che permettono al server di verificare crittograficamente che la richiesta provenga da un'app genuina e non modificata.\n\n## Checklist finale prima della release\n\n- [ ] Build con `--obfuscate` e `--split-debug-info`, simboli archiviati in CI\n- [ ] Nessuna chiave o URL hardcoded: tutto via `--dart-define-from-file`\n- [ ] Token e credenziali salvati in storage cifrato, mai in `SharedPreferences`\n- [ ] Certificate pinning attivo con pin di backup e kill switch\n- [ ] `cleartextTrafficPermitted=\"false\"` e nessuna eccezione ATS\n- [ ] `allowBackup=\"false\"` su Android\n- [ ] Logging disattivato o sanificato in release\n- [ ] File `.env`, keystore e `*.symbols` esclusi dal repository\n- [ ] Dipendenze aggiornate e verificate (`flutter pub outdated`)\n\n## Conclusione\n\nLa sicurezza di un'app Flutter è il risultato di più livelli che si rinforzano a vicenda: l'offuscamento alza il costo del reverse engineering, il pinning protegge il canale, la gestione corretta dei segreti riduce la superficie d'attacco e l'hardening nativo chiude le porte lasciate aperte dai default di sviluppo.\n\nNessuna di queste misure è sufficiente da sola, e nessuna sostituisce la regola fondamentale: **l'autorizzazione si valida sul server**. Il client va trattato come un ambiente potenzialmente ostile, e progettato di conseguenza.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F3bf1fc13-3a6d-4014-9ecb-761fe70f3cb6.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1603899122361-e99b4f6fecf5?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODg0OTQ1MTB8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Franck","https:\u002F\u002Funsplash.com\u002F@franckinjapan","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fblack-android-smartphone-displaying-green-and-black-logo-sNvBTRQR7eE",null,"published","2026-09-04T04:01:50+00:00","Sicurezza app Flutter: offuscamento e pinning","Guida pratica alla sicurezza delle app Flutter: offuscamento del codice Dart, certificate pinning con Dio, gestione dei segreti e hardening nativo.",{"id":21,"name":22,"slug":23},3,"Best practice","best-practice",{"id":25,"name":26},1,"Flutter Bot",[],1789120580531]