Bỏ qua để đến nội dung

OWASP cho Mobile

Nói tới bảo mật mobile, gần như mọi yêu cầu từ khách hàng, ngân hàng hay đơn vị pentest đều dẫn về OWASP. Bài này bắt đầu bằng một ví dụ “soi” một app thật, sau đó mới đi vào các tài liệu của OWASP và cách áp dụng.

App MyShop (Flutter, có cả Android và iOS) sắp phát hành. Trước khi lên store, một bạn trong team thử đóng vai kẻ tấn công: tải file APK/IPA về, giải nén, đọc thử. Không cần công cụ phức tạp, chỉ trong một buổi chiều đã thấy 6 vấn đề.

Terminal window
unzip -o myshop.apk -d myshop_apk
strings myshop_apk/lib/arm64-v8a/libapp.so | grep -iE "sk_live|secret|api[_-]?key"
# sk_live_51Hxxxxxxxxxxxxxxxxxxxxxxxx

Team để secret key của cổng thanh toán trong code Dart để gọi thẳng API thanh toán từ app. Mọi thứ đóng gói trong app, kể cả code đã obfuscate hay giá trị truyền qua --dart-define, đều đọc được. Ai có key này có thể gọi API thanh toán dưới danh nghĩa của bạn.

Sửa: secret chỉ nằm ở server. App gọi server của mình, server mới gọi bên thứ ba. Key nào bắt buộc phải nằm trong app (Google Maps, Firebase…) thì giới hạn nó theo package name + SHA-1 / bundle ID và chỉ bật đúng API cần dùng.

→ OWASP M1: Improper Credential Usage.

2. Token đăng nhập lưu dạng văn bản thường

Phần tiêu đề “2. Token đăng nhập lưu dạng văn bản thường”
// SAI
final prefs = await SharedPreferences.getInstance();
await prefs.setString('access_token', token);

SharedPreferences trên Android là file XML, UserDefaults trên iOS là file plist, đều không mã hoá. Máy root/jailbreak, bản backup, hoặc một lỗ hổng khác đọc được file là lấy được token.

Sửa: dùng kho lưu trữ an toàn của hệ điều hành: Keychain trên iOS, Android Keystore (qua EncryptedSharedPreferences hoặc tự mã hoá bằng key trong Keystore) trên Android. Với Flutter là flutter_secure_storage:

const storage = FlutterSecureStorage();
await storage.write(key: 'access_token', value: token);

→ OWASP M9: Insecure Data Storage.

debugPrint('Login OK: ${response.data}'); // in cả access_token, refresh_token

Trên Android, log của app có thể bị đọc qua adb logcat khi cắm máy; các công cụ báo lỗi (crash reporting) cũng có thể gửi log này về server bên thứ ba.

Sửa: không bao giờ log token, mật khẩu, thông tin cá nhân. Tắt log chi tiết của HTTP client ở bản release (ví dụ chỉ thêm interceptor log của Dio khi kDebugMode).

→ OWASP M6: Inadequate Privacy Controls, M9: Insecure Data Storage.

4. Bỏ qua kiểm tra chứng chỉ SSL “cho tiện test”

Phần tiêu đề “4. Bỏ qua kiểm tra chứng chỉ SSL “cho tiện test””
// SAI: chấp nhận mọi chứng chỉ, để test với server staging dùng chứng chỉ tự ký
HttpClient()..badCertificateCallback = (cert, host, port) => true;

Đoạn này lọt vào bản release. Bất kỳ ai ở cùng mạng Wi-Fi quán cà phê có thể đứng giữa (man-in-the-middle) đọc và sửa toàn bộ dữ liệu.

Sửa: xoá hẳn. Muốn test với chứng chỉ riêng thì cấu hình theo nền tảng, chỉ cho bản debug: network_security_config.xml với <debug-overrides> trên Android; trên iOS cài chứng chỉ vào máy test. Đồng thời đảm bảo không còn http:// (Android cleartextTrafficPermitted="false", iOS không thêm ngoại lệ ATS).

→ OWASP M5: Insecure Communication.

Màn hình “Đơn hàng của tôi” gọi GET /orders/123. App chỉ hiện đơn của người đang đăng nhập, nhưng server không kiểm tra đơn 123 có thuộc người đó không. Đổi số trong request (bằng proxy như Burp Suite, mitmproxy) là xem được đơn của người khác.

Sửa: mọi kiểm tra quyền phải ở server. Ẩn nút, ẩn màn hình ở app chỉ là giao diện, không phải bảo mật.

→ OWASP M3: Insecure Authentication/Authorization.

Phần tiêu đề “6. Bản release vẫn để chế độ debug, deep link không kiểm tra dữ liệu”
  • Android build release vẫn còn android:debuggable="true" do cấu hình nhầm build type; android:allowBackup="true" mặc định khiến dữ liệu app nằm trong bản backup.
  • Một Activity không cần thiết có android:exported="true", app khác gọi thẳng vào được.
  • Deep link myshop://pay?amount=...&to=... mở thẳng màn hình chuyển tiền đã điền sẵn và tự xác nhận.

Sửa: rà AndroidManifest.xml của bản release (xem file manifest đã merge trong Android Studio), chỉ export đúng những gì cần, tắt backup hoặc cấu hình loại trừ dữ liệu nhạy cảm. Dữ liệu từ link luôn là dữ liệu không tin cậy (xem phần lưu ý trong bài Universal Links và App Links).

→ OWASP M8: Security Misconfiguration, M4: Insufficient Input/Output Validation.

Sáu lỗi trên đều có trong danh sách OWASP Mobile Top 10. Phần còn lại của bài giải thích danh sách này và các tài liệu đi kèm.

OWASP (Open Worldwide Application Security Project) là tổ chức phi lợi nhuận về bảo mật phần mềm, nổi tiếng với các danh sách “Top 10”. Với mobile, OWASP có hai dự án:

Tài liệuLà gìDùng khi nào
Mobile Top 10Danh sách 10 nhóm rủi ro phổ biến nhấtNắm tổng quan, đào tạo team, nói chuyện với người không chuyên
MASVS (Mobile Application Security Verification Standard)Tiêu chuẩn: danh sách yêu cầu (control) app cần đạtLàm yêu cầu bảo mật cho dự án, tiêu chí nghiệm thu
MASWE (Mobile Application Security Weakness Enumeration)Danh sách các điểm yếu cụ thể, nối control của MASVS với bài testTra cứu một điểm yếu cụ thể
MASTG (Mobile Application Security Testing Guide)Hướng dẫn kiểm thử chi tiết: kỹ thuật, công cụ, bài test cho từng controlPentest, tự kiểm tra app

Ba tài liệu MASVS, MASWE, MASTG thuộc dự án OWASP MAS (Mobile Application Security) và liên kết với nhau: mỗi bài test trong MASTG gắn với một điểm yếu trong MASWE, mỗi điểm yếu gắn với một control trong MASVS.

Top 10 là “biết mình hay sai ở đâu”, MASVS là “phải đạt những gì”, MASTG là “kiểm tra bằng cách nào”.

Bản 2024 là lần cập nhật đầu tiên kể từ 2016.

#Rủi roÝ chính với mobile
M1Improper Credential UsageSecret, API key, mật khẩu nằm trong app hoặc dùng sai cách
M2Inadequate Supply Chain SecurityThư viện, SDK bên thứ ba có lỗ hổng hoặc độc hại; quy trình build bị xâm nhập
M3Insecure Authentication/AuthorizationXác thực yếu, kiểm tra quyền chỉ ở app, session không hết hạn
M4Insufficient Input/Output ValidationDữ liệu từ deep link, WebView, file, server… không được kiểm tra
M5Insecure CommunicationHTTP thường, bỏ qua kiểm tra chứng chỉ, gửi dữ liệu nhạy cảm qua kênh không an toàn
M6Inadequate Privacy ControlsThu thập hoặc làm lộ dữ liệu cá nhân quá mức cần
M7Insufficient Binary ProtectionsApp dễ bị dịch ngược, sửa đổi, đóng gói lại
M8Security MisconfigurationCấu hình sai: debug, backup, component exported, quyền thừa
M9Insecure Data StorageLưu dữ liệu nhạy cảm không mã hoá, lộ qua log, cache, backup
M10Insufficient CryptographyThuật toán yếu, tự chế mã hoá, quản lý key sai

So với 2016: M1 và M2 là nhóm mới; M7 gộp hai nhóm cũ “Reverse Engineering” và “Code Tampering”; M8 thay nhóm “Extraneous Functionality”.

Dưới đây là cách hiểu và cách phòng tránh từng nhóm trên Android, iOS và Flutter.

  • Nguyên tắc: coi như mọi thứ trong app đều bị đọc được. Obfuscation, mã hoá chuỗi, giấu trong native code chỉ làm chậm kẻ tấn công, không ngăn được.
  • Secret của server (khoá thanh toán, khoá quản trị, service account) không bao giờ đưa vào app.
  • Không lưu mật khẩu người dùng; lưu token và làm mới bằng refresh token.
  • Với Flutter: --dart-define và file .env đóng gói vào app đều không phải nơi giữ bí mật.
  • Chỉ dùng package/SDK có nguồn gốc rõ ràng, còn được bảo trì. Trên pub.dev xem publisher đã xác minh, điểm, ngày cập nhật.
  • Khoá phiên bản (commit file pubspec.lock, Podfile.lock, Gradle lockfile hoặc version catalog), cập nhật có kiểm soát.
  • Theo dõi lỗ hổng của dependency (Dependabot, OSV, flutter pub outdated).
  • Bảo vệ quy trình build: quyền truy cập CI, secret ký app (xem bài Google Play App Signing và Certificate và Provisioning Profile).
  • SDK bên thứ ba thu thập gì thì app của bạn chịu trách nhiệm: cần khai báo trong Privacy Manifest (iOS) và Data safety (Google Play).
  • Mọi kiểm tra quyền ở server.
  • Token ngắn hạn, có refresh token, thu hồi được khi đăng xuất hoặc đổi mật khẩu.
  • Đăng nhập bằng sinh trắc học: dùng sinh trắc học để mở khoá một key trong Keystore/Keychain (Android BiometricPrompt với CryptoObject, iOS Keychain với SecAccessControl), không chỉ dựa vào kết quả true/false của hàm xác thực, vì kết quả đó dễ bị sửa trên máy đã root/jailbreak.
  • OAuth: dùng Authorization Code + PKCE, mở trang đăng nhập bằng trình duyệt hệ thống (Custom Tabs, ASWebAuthenticationSession), không dùng WebView.
  • Dữ liệu từ deep link, push notification, clipboard, file người dùng chọn, QR code, và cả response của server đều phải kiểm tra.
  • WebView: tắt JavaScript nếu không cần; với addJavascriptInterface (Android) / WKScriptMessageHandler (iOS) / JavaScriptChannel (Flutter) chỉ cho trang tin cậy gọi; không cho WebView truy cập file cục bộ.
  • Truy vấn SQLite dùng tham số, không nối chuỗi.
  • Chỉ HTTPS. Android: network_security_config.xml với cleartextTrafficPermitted="false" (mặc định từ Android 9). iOS: App Transport Security, không thêm NSAllowsArbitraryLoads.
  • Không bao giờ tắt kiểm tra chứng chỉ trong code.
  • Certificate pinning giúp chống trường hợp kẻ tấn công cài được chứng chỉ giả vào máy, nhưng có chi phí vận hành: chứng chỉ server đổi mà app chưa cập nhật pin là app mất kết nối. Nếu pin, nên pin public key (không pin cả chứng chỉ), luôn có pin dự phòng, và có kế hoạch xoay vòng.
  • Chỉ xin quyền khi cần, đúng lúc cần, và giải thích lý do.
  • Không gửi dữ liệu cá nhân (email, số điện thoại, vị trí) vào analytics, log, crash report.
  • Ẩn nội dung nhạy cảm khi app vào background (ảnh chụp màn hình trong trình chuyển app): Android FLAG_SECURE, iOS che màn hình khi sceneWillResignActive.
  • Khai báo trung thực trong Privacy Nutrition Label (App Store), Data safety (Google Play).
  • Bật thu gọn và làm rối code: Android R8 (minifyEnabled true), Flutter --obfuscate --split-debug-info=<thư mục>.
  • Với app có giá trị cao (ngân hàng, game có giao dịch), cân nhắc phát hiện root/jailbreak, phát hiện debugger, hook (Frida), app bị đóng gói lại.
  • Xác minh app và thiết bị từ phía server: Play Integrity API (Android), App Attest (iOS). Server mới là nơi ra quyết định, vì kiểm tra trong app luôn có thể bị vượt qua.
  • Tất cả các lớp bảo vệ này làm chậm kẻ tấn công chứ không chặn được hẳn. Không dùng chúng thay cho việc không đưa secret vào app (M1) và kiểm tra quyền ở server (M3).

Rà soát bản release:

  • Android: không debuggable; allowBackup hoặc dataExtractionRules loại trừ dữ liệu nhạy cảm; chỉ exported="true" cho component thật sự cần; ContentProvider có permission; không xin quyền thừa.
  • iOS: không để lại cấu hình ATS nới lỏng, URL scheme, entitlement không dùng.
  • Server/Backend-as-a-Service: Firebase Realtime Database/Firestore/Storage có security rules đúng (không để allow read, write: if true).
  • Tắt màn hình debug, menu ẩn, endpoint test trong bản release.
  • Dữ liệu nhạy cảm (token, khoá) lưu trong Keychain/Keystore. Dữ liệu lớn cần bảo vệ thì mã hoá bằng key nằm trong Keychain/Keystore (ví dụ SQLCipher).
  • Không ghi dữ liệu nhạy cảm ra bộ nhớ ngoài (external storage) trên Android.
  • Để ý các nơi “rò rỉ”: log, cache HTTP, cache bàn phím (tắt gợi ý cho ô mật khẩu), clipboard, ảnh chụp màn hình app switcher, file tạm.
  • iOS: dùng Data Protection (NSFileProtectionComplete) cho file nhạy cảm; chọn mức kSecAttrAccessible phù hợp cho Keychain (ví dụ ...ThisDeviceOnly để không đi theo bản backup sang máy khác).
  • Không tự chế thuật toán. Dùng thư viện chuẩn của nền tảng (Android Keystore + javax.crypto, iOS CryptoKit).
  • Không dùng MD5, SHA-1 cho mục đích bảo mật; không dùng AES ở chế độ ECB; dùng AES-GCM.
  • Key không hardcode trong app, sinh và giữ trong Keystore/Keychain (có phần cứng bảo vệ như StrongBox, Secure Enclave khi có).
  • Về lâu dài, để ý xu hướng mật mã hậu lượng tử (ví dụ Android 17 đã hỗ trợ ML-DSA trong Keystore, xem phần quantum-ready key trong bài Google Play App Signing).

MASVS: tiêu chuẩn để làm yêu cầu dự án

Phần tiêu đề “MASVS: tiêu chuẩn để làm yêu cầu dự án”

Top 10 tốt để nhận thức, nhưng khi cần tiêu chí nghiệm thu (hợp đồng với khách hàng, yêu cầu của ngân hàng, kiểm định), nên dùng MASVS. Bản hiện tại là MASVS v2.1.0, gồm 8 nhóm control:

NhómNội dung
MASVS-STORAGELưu trữ an toàn dữ liệu nhạy cảm
MASVS-CRYPTODùng mật mã đúng cách
MASVS-AUTHXác thực và phân quyền
MASVS-NETWORKGiao tiếp mạng an toàn
MASVS-PLATFORMTương tác an toàn với nền tảng (IPC, WebView, UI)
MASVS-CODEChất lượng code: cập nhật, dependency, kiểm tra dữ liệu đầu vào
MASVS-RESILIENCEChống dịch ngược, sửa đổi app
MASVS-PRIVACYQuyền riêng tư người dùng (thêm từ v2.1.0)

MASVS v2 bỏ các “level” cũ (L1, L2, R) trong bản thân tiêu chuẩn. Thay vào đó, MASTG dùng testing profile để chọn mức kiểm thử phù hợp:

  • MAS-L1: mức cơ bản, cho hầu hết app.
  • MAS-L2: cho app xử lý dữ liệu nhạy cảm (ngân hàng, y tế), cần phòng thủ nhiều lớp.
  • MAS-R: chống dịch ngược và sửa đổi, cho app cần bảo vệ tài sản trí tuệ hoặc chống gian lận (game, DRM).
  • MAS-P: các bài test về quyền riêng tư.

Cách dùng thực tế: chọn profile theo loại app, lấy danh sách control tương ứng làm checklist từ đầu dự án, không đợi tới lúc pentest mới đối chiếu.

Một số công cụ thường dùng để tự soi app của mình trước khi gửi pentest (chỉ dùng trên app của mình hoặc app được phép kiểm thử):

Công cụDùng để
MobSFQuét tĩnh APK/IPA tự động: quyền, cấu hình manifest/plist, secret trong code, thư viện
jadxDịch ngược APK sang Java/Kotlin để đọc
apktoolGiải nén APK, đọc manifest và resource đã biên dịch
Burp Suite, mitmproxyProxy chặn và sửa request giữa app và server
Frida, objectionHook app lúc chạy, thử vượt qua các lớp bảo vệ
Android Lint, các lint bảo mậtBắt lỗi cấu hình ngay khi code

Với Flutter: code Dart biên dịch thành mã máy trong libapp.so (Android) và App.framework (iOS) nên jadx không đọc được logic Dart, nhưng strings vẫn lộ chuỗi, và đã có công cụ chuyên phân tích file snapshot của Dart. Đừng coi việc “khó dịch ngược” là lớp bảo vệ.

Trước mỗi lần phát hành:

  • Không có secret của server trong app (quét strings, MobSF).
  • Token lưu trong Keychain/Keystore, không trong SharedPreferences/UserDefaults.
  • Không log token, mật khẩu, dữ liệu cá nhân ở bản release.
  • Không còn code bỏ qua kiểm tra chứng chỉ; không cho phép HTTP thường.
  • Manifest release: không debuggable, backup đã cấu hình, chỉ export component cần thiết.
  • Dữ liệu từ deep link, WebView, push đều được kiểm tra; hành động nhạy cảm cần người dùng xác nhận.
  • Đã bật R8/obfuscate và lưu file mapping/symbol để đọc crash.
  • Đã cập nhật dependency có lỗ hổng đã biết.
  • Khai báo quyền riêng tư (Privacy Manifest, Data safety) khớp với những gì app và SDK thu thập.
  • Phía server: kiểm tra quyền trên từng API, security rules của Firebase đúng.