Irbisa · Шпаргалка

Безопасность

Безопасное хранилище, certificate pinning, обфускация, OWASP Mobile Top 10

Senior-разработчик · 24 вопроса

  1. 01

    Какие ключевые практики безопасности нужны Flutter-приложению и как они ложатся на OWASP Mobile Top 10?

    Считайте клиент враждебным: настоящие секреты держите на сервере, долгоживущие токены в Keychain или Keystore, общайтесь только по HTTPS, а каждое решение об авторизации пусть принимает сервер.

  2. 02

    Как безопасно хранить токены и другие чувствительные данные на устройстве?

    Токены и ключи кладите в flutter_secure_storage (он отдаёт их в Keychain на iOS или в шифр на ключе из Keystore на Android), access-токен держите в памяти, а пароль не храните вовсе.

    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

    Посмотрите на этот код входа: какие проблемы с безопасностью вы видите и как исправить каждую?

    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');}

    Четыре проблемы: токены лежат в обычном файле настроек, access-токен печатается в лог устройства, ключ подписи берётся из предсказуемого генератора, а выход из аккаунта оставляет refresh-токен, так что сессия переживает логаут.

  4. 04

    Сторонний сервис выдал API-ключ и просит положить его в приложение. Может ли секрет жить в бинарнике и где ему место?

    Сначала определите, какой это ключ: публикуемый идентификатор может ехать в сборку, настоящий секрет не может никогда.

    // 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

    Что такое App Transport Security и Network Security Config и когда их приходится трогать во Flutter-приложении?

    Это сетевые правила уровня ОС, которые применяют платформенные HTTP-стеки вроде URLSession, OkHttp и WebView: обе системы по умолчанию блокируют обычный HTTP и позволяют добавить узкие исключения по доменам.

    <!-- 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

    Как работает code obfuscation во Flutter и как символизировать краши после его включения?

    Обфускация заменяет имена классов, методов и полей в релизном бинарнике на короткие бессмысленные токены, поэтому декомпилированное приложение трудно читать.

    # 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

    Как избежать утечки чувствительных данных через логи, скриншоты и буфер обмена?

    Не логируйте секреты, блокируйте захват экрана на чувствительных экранах и не кладите секреты в буфер обмена.

    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

    Приложение декодирует JWT, видит в claim role значение admin и показывает админскую вкладку, а сервер роль не перепроверяет. Почему пентестер записывает это в находки?

    Потому что спрятанная вкладка не является контролем доступа: админский эндпоинт никуда не делся, а атакующий дёргает эндпоинт, а не ваше дерево виджетов.

    // 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

    Поле поиска отдаёт текст напрямую в sqflite rawQuery, а загрузка сохраняет файл под именем, которое вернул API; что чините первым?

    Обе проблемы сводятся к одной и той же ошибке: внешний ввод вклеивается в язык (SQL в одном случае, путь к файлу в другом) вместо передачи как значения.

    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 вернул true после промпта Face ID. Что это на самом деле доказало и как превратить это в реальную защиту?

    Это доказало только, что ОС сказала «да» на этом устройстве, прямо сейчас, в вашем процессе.

  11. 11

    Какие подводные камни безопасности у использования WebView во Flutter-приложении?

    WebView выполняет веб-контент с правами вашего процесса, поэтому неправильно настроенный WebView сводит на нет secure storage, pinning и песочницу.

    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

    Почему deep link считается недоверенным вводом и что мешает другому приложению перехватить ваши ссылки?

    Deep link приходит снаружи с параметрами, которые задаёт атакующий, а у обычной кастомной схемы нет владельца: myapp:// может зарегистрировать любое приложение.

    // 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

    Найдите баг: этот интерцептор должен добавлять токен только к нашему API, но токен всплывает в логах чужого сервера.

    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);  }}

    Проверка хоста сравнивает строковые префиксы, поэтому https://api.example.com.evil.io/x.png или https://api.example.com@evil.io/x.png её проходят, и любой URL, который выбрал атакующий и который загружается через этот Dio, получает bearer-токен пользователя.

  14. 14

    Коллега хочет добавить pub-пакет с 30 лайками для работы с платежами. Как принять решение и как потом удержать дерево зависимостей безопасным?

    Оценивайте пакет по тому, как его поддерживают и что он может сделать внутри вашего приложения, а не по лайкам; для платежей берите официальный SDK вендора.

  15. 15

    Пользователи разлогиниваются после переезда на новый телефон, а на Android пара запусков падает с ошибкой расшифровки токена. Что произошло?

    Зашифрованный блоб попал в бэкап и восстановился, а ключ к нему нет: flutter_secure_storage пишет шифротекст в обычный файл, а ключ лежит в Android Keystore и устройство не покидает.

    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

    Что такое SSL/certificate pinning и как реализовать его во Flutter?

    Certificate pinning означает, что приложение доверяет только сертификату или публичному ключу вашего сервера, а не любому сертификату, которому доверяет устройство.

    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

    Этот клиент должен пиннить сертификат API. Остановит ли он прокси, CA которого доверен на устройстве, и что бы вы изменили?

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

    Нет. badCertificateCallback вызывается только после того, как обычная проверка провалилась, поэтому сертификат прокси, подписанный доверенным на устройстве CA, проверку проходит и до сравнения пина не доходит: код пиннит только уже отвергнутые сертификаты.

  18. 18

    Как работает OAuth 2.0 с PKCE для мобильных приложений и почему он обязателен?

    PKCE позволяет мобильному приложению безопасно пройти OAuth code flow без 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

    Как спроектировать ротацию access- и refresh-токенов и как обнаружение повторного использования ловит украденный refresh-токен?

    Для запросов используйте короткоживущий access-токен (минуты), а для получения новых нужен более долгий refresh-токен, и при каждом обновлении выдавайте новый refresh-токен.

  20. 20

    Найдите баг: после часа в фоне приложение при открытии часто разлогинивает пользователя, хотя refresh-токен ещё действителен.

    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);    }  }));}

    Каждый запрос, получивший 401, запускает собственный refresh.

  21. 21

    Как определить рутованное или джейлбрейкнутое устройство и почему тестировщик с Frida всё равно обходит вашу проверку?

    Локальные проверки (бинарник su, пути Magisk или Cydia, сборка с test-keys, признаки эмулятора) легко написать и так же легко перехватить: Frida переписывает их результат прямо в вашем процессе.

  22. 22

    Коллега шифрует офлайн-кеш документов через AES ключом, выведенным из email пользователя, и константным IV из туториала. Что здесь сломано?

    Ключ, который может вычислить кто угодно, ключом не является, а фиксированный IV делает одинаковые документы одинаковым шифротекстом; вместе это обфускация, а не шифрование.

    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

    Хранилище токенов зашифровано, но криминалисты достали вложения из чатов и кеш картинок с заблокированного тестового телефона. Как данные оказались доступны?

    Шифрование диска защищает только выключенный телефон: после первой разблокировки с момента загрузки ключи остаются в памяти, а всё вне flutter_secure_storage живёт ровно под этим дефолтом.

    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

    Модифицированная сборка вашего приложения открывает премиум без оплаты, хотя код покупок выглядит правильным. Где неверно проведена граница доверия?

    Премиум выдаёт устройство, а на устройстве работает код атакующего.

    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;…