Irbisa · Cheatsheet

Security

Secure storage, certificate pinning, obfuscation, OWASP mobile top 10

Senior Developer · 24 questions

  1. 01

    What are the key security practices for a Flutter app, and how do they map to the OWASP Mobile Top 10?

    Treat the client as hostile: keep real secrets on the server, keep long-lived tokens in the Keychain or Keystore, talk HTTPS only, and let the server make every authorization decision.

  2. 02

    How do you securely store tokens and other sensitive data on the device?

    Put tokens and keys in flutter_secure_storage, which hands them to the iOS Keychain or to a Keystore-backed cipher on Android, keep the access token in memory, and never store the password.

    import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class TokenStore {  TokenStore([this._storage = const FlutterSecureStorage()]);  final FlutterSecureStorage _storage;  String? _access; // memory only: gone when the process dies…
  3. 03

    Review this sign-in code: what security problems do you see, and how would you fix each one?

    Future<void> onSignedIn(String accessToken, String refreshToken) async {  final prefs = await SharedPreferences.getInstance();  await prefs.setString('access_token', accessToken);  await prefs.setString('refresh_token', refreshToken);  debugPrint('signed in, token=$accessToken');  final requestKey = Random().nextInt(1 << 32).toString(); // later used to sign requests  await prefs.setString('request_key', requestKey);} Future<void> signOut() async {  final prefs = await SharedPreferences.getInstance();  await prefs.remove('access_token');}

    Four problems: the tokens sit in a plain preferences file, the access token is printed to the device log, a signing key comes from a predictable generator, and sign-out leaves the refresh token behind, so the session survives logout.

  4. 04

    A third-party service gives you an API key to put in the app. Can a secret live in the binary, and where does it belong?

    First decide which kind of key it is: a publishable identifier can ship, a real secret never does.

    // right: Publishable key (compiled in, restricted server-side, safe to ship)class Config {  static const mapsKey = String.fromEnvironment('MAPS_KEY');  static const apiBase = String.fromEnvironment(    'API_BASE', defaultValue: 'https://api.example.com',  );…
  5. 05

    What are App Transport Security and Network Security Config, and when do you have to touch them in a Flutter app?

    They are OS-level network rules enforced by platform HTTP stacks such as URLSession, OkHttp and WebView: both block plain HTTP by default and allow narrow per-domain exceptions.

    <!-- iOS: ios/Runner/Info.plist - plaintext for one legacy host only --><key>NSAppTransportSecurity</key><dict>  <key>NSExceptionDomains</key>  <dict>    <key>legacy-sensor.example.com</key>…
  6. 06

    How does code obfuscation work in Flutter, and how do you symbolicate crashes after enabling it?

    Obfuscation replaces class, method and field names in the release binary with short meaningless tokens, so a decompiled app is hard to read.

    # 1) Build a release with obfuscation + split debug infoflutter build apk --release \  --obfuscate \  --split-debug-info=build/symbols/v1_2_3 flutter build ipa --release \…
  7. 07

    How do you avoid leaking sensitive data through logs, screenshots, and clipboard?

    Do not log secrets, block screen capture on sensitive screens, and keep secrets out of the clipboard.

    import 'package:flutter/material.dart'; // App switcher: cover the UI while inactive, since iOS has no FLAG_SECURE.// Use as MaterialApp(builder: (_, child) => PrivacyCurtain(child: child!)).// Android: window.setFlags(FLAG_SECURE, FLAG_SECURE) in MainActivity instead.class PrivacyCurtain extends StatefulWidget {…
  8. 08

    Your app decodes the JWT, reads a role claim of admin and shows the admin tab, and the server does not re-check the role. Why is that a finding in a pentest report?

    Because hiding a tab is not access control: the admin endpoint is still there, and an attacker calls the endpoint, not your widget tree.

    // wrong: The tab is hidden; the endpoint is wide openfinal claims = JwtDecoder.decode(accessToken);if (claims['role'] == 'admin') {  tabs.add(const AdminTab()); // repackaged build: force this branch, call the API}…
  9. 09

    A search box feeds its text straight into a sqflite rawQuery, and a download saves under the filename the API returned; what do you fix first?

    Both are the same bug: outside input is glued into a language (SQL in one case, a file path in the other) instead of being passed as a value.

    import 'package:path/path.dart' as p;import 'package:sqflite/sqflite.dart'; // wrong: One apostrophe changes the statement; a crafted string changes the appFuture<List<Map<String, Object?>>> searchBad(Database db, String q, String sort) =>    db.rawQuery("SELECT * FROM notes WHERE title LIKE '%$q%' ORDER BY $sort");…
  10. 10

    local_auth returns true after a Face ID prompt. What has that actually proven, and how do you turn it into real protection?

    It proved only that the OS said yes on this device, right now, in your process.

  11. 11

    What are the security pitfalls of using WebView in a Flutter app?

    A WebView runs web content with the trust of your app process, so a misconfigured one can undo secure storage, pinning and the sandbox.

    import 'package:webview_flutter/webview_flutter.dart'; // right: Constrained WebView (explicit allowlist, no JS bridge unless needed)class HelpCenterPage extends StatefulWidget {  const HelpCenterPage({super.key});  @override State<HelpCenterPage> createState() => _HelpCenterPageState();…
  12. 12

    Why is a deep link untrusted input, and what stops another app from stealing yours?

    A deep link comes from outside your app with parameters an attacker can set, and a plain custom scheme has no owner: any app can register myapp:// too.

    // go_router: one guarded entry point for every incoming link// session: a ChangeNotifier that knows whether the user is signed infinal router = GoRouter(  refreshListenable: session,  redirect: (context, state) {    final loggedIn = session.isSignedIn;…
  13. 13

    Find the bug: this interceptor should add the token only for our API, yet the token turns up in the logs of a server we do not own.

    const apiBase = 'https://api.example.com'; class AuthInterceptor extends Interceptor {  AuthInterceptor(this.tokens);  final TokenStore tokens;   @override  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {    // only our own API gets the token    if (options.uri.toString().startsWith(apiBase)) {      options.headers['Authorization'] = 'Bearer ${tokens.access}';    }    handler.next(options);  }}

    The host check compares string prefixes, so https://api.example.com.evil.io/x.png or https://api.example.com@evil.io/x.png passes it, and any attacker-chosen URL fetched through this Dio receives the user's bearer token.

  14. 14

    A teammate wants to add a pub package with 30 likes to handle payments. How do you decide, and how do you keep the dependency tree safe over time?

    Judge a package by how it is maintained and what it can do inside your app, not by its likes; for payments, prefer the vendor's official SDK.

  15. 15

    Users are signed out after moving to a new phone, and a few Android launches throw a decryption error reading the token. What happened?

    The encrypted blob was backed up and restored, but the key that opens it was not: flutter_secure_storage writes ciphertext into a normal file, while the key lives in the Android Keystore and cannot leave the device.

    import 'package:flutter/services.dart';import 'package:flutter_secure_storage/flutter_secure_storage.dart';import 'package:shared_preferences/shared_preferences.dart'; const _storage = FlutterSecureStorage(  iOptions: IOSOptions(…
  16. 16

    What is SSL/certificate pinning and how do you implement it in Flutter?

    Certificate pinning means the app trusts only your server's certificate or public key, not every certificate the device happens to trust.

    import 'dart:io';import 'package:dio/dio.dart';import 'package:dio/io.dart';import 'package:flutter/services.dart'; /// Trusts ONLY the CA bundled with the app: any other chain, including a…
  17. 17

    This client is meant to pin the API certificate. Does it stop a proxy whose CA the device trusts, and what would you change?

    HttpClient pinnedClient() {  final client = HttpClient();  client.badCertificateCallback = (cert, host, port) {    final pin = base64Encode(sha256.convert(cert.der).bytes);    return pin == kApiCertPin;  };  return client;}

    No. badCertificateCallback runs only after normal validation has failed, so a proxy certificate signed by a CA the device trusts passes validation and never reaches the pin check: the code pins only certificates that were already rejected.

  18. 18

    How does OAuth 2.0 with PKCE work for mobile apps and why is it required?

    PKCE lets a mobile app do the OAuth code flow safely without a client secret.

    import 'dart:convert';import 'dart:math';import 'package:crypto/crypto.dart';import 'package:flutter_appauth/flutter_appauth.dart'; // What PKCE adds, by hand: a random verifier and its S256 challenge…
  19. 19

    How do you design access- and refresh-token rotation, and how does reuse detection catch a stolen refresh token?

    Use a short-lived access token (minutes) for requests and a longer-lived refresh token to get new ones, and issue a brand-new refresh token on every refresh.

  20. 20

    Find the bug: after an hour in the background, opening the app often signs the user out, though the refresh token is still valid.

    void addAuth(Dio dio, TokenStore tokens) {  dio.interceptors.add(InterceptorsWrapper(onError: (err, handler) async {    final req = err.requestOptions;    if (err.response?.statusCode != 401 || req.extra['retried'] == true) {      return handler.next(err);    }    try {      final pair = await tokens.refresh(); // POST /auth/refresh, the server rotates the pair      req..extra['retried'] = true..headers['Authorization'] = 'Bearer ${pair.access}';      handler.resolve(await dio.fetch(req));    } on DioException catch (e) {      handler.reject(e);    }  }));}

    Each request that gets a 401 starts its own refresh.

  21. 21

    How do you detect a rooted or jailbroken device, and why can a tester with Frida still defeat your check?

    Local checks (su binaries, Magisk or Cydia paths, test-keys builds, emulator traits) are easy to write and just as easy to hook, because Frida rewrites their result inside your process.

  22. 22

    A teammate encrypts the offline document cache with AES, using a key derived from the user's email and a constant IV copied from a tutorial. What breaks?

    A key anyone can compute is not a key, and a fixed IV makes identical documents produce identical ciphertext; together this is obfuscation, not encryption.

    import 'dart:convert';import 'dart:math';import 'dart:typed_data';import 'package:cryptography/cryptography.dart';import 'package:flutter_secure_storage/flutter_secure_storage.dart';…
  23. 23

    The token store is encrypted, yet a forensics report pulled chat attachments and cached images off a locked test phone. How did they get out?

    Platform disk encryption only protects a phone that is off: after the first unlock since boot the keys stay in memory, and everything outside flutter_secure_storage sits under exactly that default.

    import 'dart:io';import 'package:flutter_cache_manager/flutter_cache_manager.dart';import 'package:path_provider/path_provider.dart';import 'package:webview_flutter/webview_flutter.dart'; /// Derived data belongs in Caches/tmp: not backed up, and the OS may reclaim it.…
  24. 24

    A modified build of your app unlocks the premium tier without paying, and the in-app purchase code looks correct. Where is the trust boundary wrong?

    The device is granting the premium tier, and the device runs the attacker's code.

    import 'package:dio/dio.dart';import 'package:in_app_purchase/in_app_purchase.dart'; class Purchases {  Purchases(this._iap, this._api, this._entitlements);  final InAppPurchase _iap;…