Trong quá trình phát triển sản phẩm việc mắc phải các lỗi gây ra các lỗ hổng bảo mật trong ứng dụng là điều không tránh khỏi đối với các lập trình viên chính vì vậy hôm nay mình nêu ra các lỗ hổng tiềm ẩn mà các ứng dụng thường gặp phải khi làm ứng dụng trên thiết bị android và cách phòng tránh chúng .Sau đây là những lỗ hổng thường thấy mà hacker hay khai thác
- Lưu trữ dữ liệu không an toàn
- Truyền dữ liệu không an toàn (HTTP)
- Log thông tin nhạy cảm
- Hardcoded API Keys / Secrets
- Mã nguồn dễ bị dịch ngược (Reverse engineering)
- WebView không kiểm soát
- Lộ keystore
- Kiểm Tra Tính Toàn Vẹn Lúc Chạy
- Xác thực hoặc phân quyền không an toàn
1.Lưu trữ dữ liệu không an toàn
Thực trạng : Khi phát triển ứng dụng có nhiều khi ta phải lưu trữ các giá trị lại để sử dụng ví dụ như lưu các biến vào cache hay nhiều hơn là tạo cả 1 cơ sở dữ liệu với các bảng trên mobile để lưu trữ những dữ liệu cần thiết để cá nhân hóa cho người dùng trên ứng dụng .Những khi làm như vậy chúng ta phải đối phó với nguy cơ lộ lọt dữ liệu do hacker có thể dễ dàng tìm thấy các file lưu trữ này trong thiết bị của user và từ đó có thể lấy được toàn bộ thông tin .Những lỗi cơ bản của các lập trình viên mới khi bắt đầu là quá tin tưởng vào các thư viện lưu trữ dữ liệu kiểu này do đó đã lưu xuống cả token hoặc các thông tin quan trọng khác của người dùng .
Cách giải quyết : Vậy có cách giải quyết nào cho thực trạng này chúng ta có thể dùng một số biện pháp như Dùng mã hóa AES, hoặc EncryptedSharedPreferences, SQLCipher .Bây giờ chúng ta sẽ đi vào chi tiết của từng phương pháp :
- AES (Advanced Encryption Standard) là một thuật toán mã hóa đối xứng, nghĩa là cùng một khóa được sử dụng cho cả quá trình mã hóa và giải mã. Với các dữ liệu nhạy cảm như token hoặc secret key, ta nên mã hóa trước khi lưu trữ để đảm bảo an toàn.Khóa giải mã nên được lưu trữ an toàn, chẳng hạn như trên server hoặc trong secure storage của thiết bị.Trong Flutter, bạn có thể sử dụng thư viện encrypt để thực hiện mã hóa AES, và lưu trữ kết quả vào secure storage thông qua thư viện flutter_secure_storage.
- Dùng EncryptedSharedPreferences là một kỹ thuật bảo mật dữ liệu khi lưu trữ vào bộ nhớ máy. Khi ghi dữ liệu, thư viện sẽ tự động mã hóa dữ liệu trước khi lưu xuống, và khi lấy ra, thư viện sẽ tự động giải mã để trả về dạng text, giúp lập trình viên không cần phải xử lý thủ công việc mã hóa hoặc giải mã.Trên Android, EncryptedSharedPreferences sử dụng Android Keystore System để quản lý và bảo vệ khóa mã hóa một cách an toàn, đảm bảo khóa này không bị lộ hoặc truy cập trái phép. Tương tự, trên iOS, hệ thống sử dụng Keychain để lưu trữ khóa bảo mật này.Trong Flutter, thư viện flutter_secure_storage tận dụng chính các cơ chế bảo mật của nền tảng (Android Keystore và iOS Keychain) để lưu trữ khóa mã hóa một cách an toàn. Khi lưu trữ dữ liệu nhạy cảm bằng flutter_secure_storage, dữ liệu sẽ được mã hóa trước khi lưu xuống thiết bị dựa trên khóa bảo mật này, giúp tăng cường tính an toàn cho dữ liệu so với việc lưu thẳng vào SharedPreferences thông thường.
- Dùng SQLCipher ( trong flutter ta có thể dùng sqflite_sqlcipher ) đây là một kỹ thuật bảo mật toàn bộ cơ sở dữ liệu SQLite bằng cách sử dụng mật khẩu mã hóa. Khi mở database, thư viện sẽ yêu cầu một password và dùng nó để mã hóa hoặc giải mã toàn bộ file database. Nếu không có đúng password, không ai có thể đọc hoặc ghi dữ liệu vào file .db .Khi lưu dữ liệu, thư viện sẽ mã hóa từng byte trong database trước khi ghi xuống ổ đĩa. Khi đọc lại, thư viện sẽ giải mã toàn bộ nội dung theo password đã cung cấp. Tất cả quá trình này là tự động, bạn vẫn sử dụng SQLite như bình thường nhưng file lưu trên máy là mã hóa.Còn mật khẩu mã hóa ta có thể dùng flutter_secure_storage để lưu
2.Truyền dữ liệu không an toàn (HTTP)
Thực trạng : Khi ứng dụng cần gửi hoặc nhận dữ liệu từ server (ví dụ: đăng nhập, lấy thông tin người dùng, gửi tin nhắn...), dữ liệu đó thường được truyền qua internet. Nếu lập trình viên sử dụng giao thức HTTP không bảo mật, toàn bộ thông tin truyền đi – bao gồm tài khoản, mật khẩu, token, nội dung cá nhân... – đều sẽ không được mã hóa.Điều này cực kỳ nguy hiểm vì kẻ tấn công có thể thực hiện các kỹ thuật như "Man-in-the-Middle" (MITM) để chặn bắt dữ liệu giữa ứng dụng và server. Trong nhiều trường hợp, hacker chỉ cần kết nối vào cùng một mạng WiFi là đã có thể thấy được dữ liệu truyền qua HTTP.Đây là một lỗi cơ bản mà nhiều lập trình viên mới gặp phải, đặc biệt khi đang thử nghiệm ứng dụng với API tạm thời hoặc server tự host mà chưa bật SSL (HTTPS).Hoặc thiết bị cài app cài và tin tưởng CA (Certificate Authority) của proxy bên ngoài cũng dẫn đến bị tấn công
Ta có một ví dụ về Man-in-the-Middle với app LittleFox : Cách giải quyết: Để bảo vệ dữ liệu khi truyền tải qua mạng, có thể áp dụng các biện pháp sau:
1.Sử dụng HTTPS thay cho HTTP : HTTPS là phiên bản bảo mật của HTTP – nó sử dụng SSL/TLS để mã hóa dữ liệu truyền giữa client (ứng dụng) và server. Điều này đảm bảo:
- Hacker không thể đọc được dữ liệu truyền đi, kể cả khi bắt được gói tin.
- Dữ liệu không bị chỉnh sửa trong quá trình truyền.
- Xác minh đúng server mà ứng dụng đang kết nối tới.
2.Thêm kiểm tra chứng chỉ (certificate pinning) : Trong một số trường hợp đặc biệt, hacker có thể giả mạo chứng chỉ để đánh lừa ứng dụng. Nên ta có thể sử dụng Certificate Pinning đây là kỹ thuật giúp app chỉ chấp nhận đúng chứng chỉ SSL của server, nhằm ngăn hacker giả mạo chứng chỉ dù đã dùng HTTPS. Trong Dio, bạn có thể bật pinning bằng cách lấy SHA-256 fingerprint của chứng chỉ server rồi so sánh trong badCertificateCallback của HttpClient. Nếu fingerprint không khớp, app sẽ từ chối kết nối, bảo vệ khỏi các cuộc tấn công MITM ngay cả khi thiết bị bị root hoặc bị chặn qua proxy.Nói đơn giản là bạn là ứng dụng chỉ chấp nhận chứng chỉ đúng của server bạn.
Trong Flutter, có thể làm điều này bằng cách:
- Sử dụng thư viện dio
- Gắn cứng certificate fingerprint vào app và kiểm tra trong callback.
3.Hạn chế truyền dữ liệu nhạy cảm nếu không cần thiết
- Không nên gửi token hoặc thông tin người dùng trong query parameters.
- Ưu tiên sử dụng HTTP Headers để truyền token (dưới dạng Bearer Token).
- Mã hóa payload quan trọng trước khi gửi đi nếu cần thêm lớp bảo mật (có thể dùng AES từ phần 1).
3.Log thông tin nhạy cảm
Thực trạng : Khi phát triển ứng dụng, việc sử dụng các câu lệnh print để ghi log là điều rất phổ biến nhằm mục đích kiểm tra luồng xử lý hoặc lỗi. Tuy nhiên, một lỗi rất nghiêm trọng mà nhiều lập trình viên (đặc biệt là người mới) hay mắc phải đó là log ra những thông tin nhạy cảm như:
Token truy cập
- Thông tin tài khoản (email, số điện thoại, mật khẩu...)
- Nội dung phản hồi từ API có chứa dữ liệu cá nhân
- Thông tin thiết bị hoặc dữ liệu người dùng Nếu không cẩn thận, những log này sẽ:
- Hiển thị trên console, có thể bị trích xuất khi thiết bị bị root/jailbreak.
- Ghi vào file log nội bộ, hacker có thể dễ dàng lấy được toàn bộ dữ liệu chỉ bằng cách xem file log hoặc dùng adb. Cách giải quyết:
1.Không log thông tin nhạy cảm : Luôn xem xét kỹ dữ liệu trước khi log. Những dữ liệu tuyệt đối không nên log ra gồm:
- Access token, refresh token
- Mã OTP, mật khẩu
- Thông tin tài khoản và nội dung phản hồi từ server có chứa dữ liệu cá nhân
2.Tạo wrapper cho logger có kiểm soát : Thay vì gọi trực tiếp print, nên tạo một lớp AppLogger có cơ chế kiểm tra môi trường nếu là production thì sẽ không show log ra
4.Hardcoded API Keys / Secrets
Thực trạng : Khi phát triển ứng dụng, rất nhiều lập trình viên có thói quen gắn trực tiếp (hardcode) các giá trị nhạy cảm như:
- API Key của dịch vụ bên thứ ba (Firebase, Google Maps, Stripe…)
- Client Secret
- Access Token
- Khóa mã hóa AES
Việc này cực kỳ nguy hiểm, bởi vì:
- Khi build ra APK hoặc IPA, các giá trị này vẫn nằm trong file nhị phân.
- Hacker chỉ cần dùng công cụ như jadx, apktool, strings… để giải mã và lấy được thông tin này.
- Các khóa bị lộ có thể bị lạm dụng để truy cập dịch vụ tính phí, gửi yêu cầu giả mạo, hoặc xâm nhập vào hệ thống backend.
Cách giải quyết:
1.Tuyệt đối không hardcode trực tiếp trong mã nguồn : Không bao giờ viết key thẳng vào code. Dù chỉ là debug hoặc để tiện lợi lúc dev cũng nên tránh. 2.Sử dụng biến môi trường (.env) : Dùng thư viện flutter_dotenv để lưu key riêng biệt bên ngoài mã nguồn trách tình trạng push key lên github hoặc nơi khác 3.Dữ liệu runtime tạm thời – dùng Secure Storage Các token hoặc key mà app cần lưu sau khi đăng nhập nên được mã hóa và lưu bằng flutter_secure_storage 4.Đối với key nhạy cảm hơn – lưu trên server Key dạng client_secret, token admin hoặc các thông tin có quyền cao không bao giờ được lưu trên app. Thay vào đó:
- App gọi API trung gian trên server
- Server giữ khóa và gửi lại kết quả (hoặc token tạm thời) cho app Ví dụ: app không gọi Stripe trực tiếp mà gọi đến backend → backend dùng secret key để xử lý thanh toán.
5. Mã nguồn dễ bị dịch ngược (Reverse engineering)
Thực trạng : Khi bạn build một ứng dụng Flutter (hoặc Android gốc), bản .apk hay .aab thực chất là một file nén chứa tất cả resource, file cấu hình, thư viện native và mã bytecode. Vì vậy, chỉ với một số công cụ như:
- apktool → Giải nén APK, xem toàn bộ file cấu hình, asset, AndroidManifest.xml
- jadx → Decompile code Java/Kotlin, xem logic xử lý gần như giống với mã gốc Hacker có thể dễ dàng:
- Xem được toàn bộ logic xử lý của app.
- Lấy các API key, chuỗi bí mật bị hardcode trong mã.
- Bypass các cơ chế bảo mật hoặc kiểm tra license.
- Tìm điểm yếu để khai thác tiếp.
Dưới đây là một demo cho việc dùng apktool giải nén apk xem thông tin trong app LittleFox
Cách giải quyết:
1.Obfuscation (Làm rối mã nguồn) Làm rối mã là bước tối thiểu để ngăn việc decompile dễ dàng. Mặc dù không thể ngăn hoàn toàn việc bị dịch ngược, nhưng nó gây khó khăn và mất thời gian hơn cho hacker. Với Flutter:
Sử dụng tùy chọn obfuscate khi build: flutter build apk --obfuscate --split-debug-info=build/symbols
- --obfuscate: làm rối mã Dart.
- --split-debug-info: tách thông tin debug ra riêng giúp giảm khả năng reverse.
- Tạo thư mục build/symbols chứa symbol map để bạn vẫn có thể debug sau này.
Với code native (Java/Kotlin):
- Cấu hình ProGuard hoặc R8 để làm rối mã native:
- Thêm rule vào file proguard-rules.pro.
- Loại trừ các class cần giữ nguyên (như activity được gọi trong manifest).
Đổi tên biến & class quan trọng bằng tay hoặc công cụ Việc đổi tên biến/class giúp gây khó hiểu cho hacker. Ví dụ: thay vì dùng userToken, hãy đổi tên thành a1, kX, v.v.
- Phát hiện thiết bị bị root / emulator
- Kiểm tra xem thiết bị có bị root hay đang chạy trên giả lập (emulator) không. Dùng các thư viện như:
- root_detector
- emulator_check
- hoặc tự viết hàm kiểm tra đường dẫn su, file xposed, thư mục /data/local/tmp, v.v.
6. WebView không kiểm soát
Thực trạng:
Khi dùng WebView để hiển thị nội dung web trong app, nhiều lập trình viên quên không cấu hình bảo mật, dẫn đến rủi ro như:
-
Cho phép thực thi JavaScript mà không kiểm soát nội dung → Dễ bị XSS.
-
Cho phép mở bất kỳ URL nào → Dễ bị redirect đến trang giả mạo (phishing).
-
Cho phép truy cập file nội bộ (file://) → Rò rỉ thông tin. Cách giải quyết:
-
Tắt JavaScript nếu không cần
-
Nếu bạn buộc phải bật JavaScript hãy giới hạn những hàm hoặc chức năng mà trang web có thể gọi từ ứng dụng để tránh bị lợi dụng hoặc tấn công.
-
Kiểm soát việc chuyển hướng URL – chỉ cho phép người dùng truy cập các đường dẫn đáng tin cậy và chặn các đường dẫn không an toàn hoặc không mong muốn.
7. Lộ keystore
Thực trạng: Keystore là nơi lưu trữ private key dùng để ký ứng dụng Android (APK/AAB). Khi bạn xuất bản ứng dụng lên Google Play, keystore là bằng chứng xác thực duy nhất cho thấy bạn là chủ sở hữu của app đó. Nếu file keystore và mật khẩu của nó bị lộ:
- Hacker có thể ký lại ứng dụng với chữ ký hợp lệ và phân phối bản app chứa mã độc.
- Google Play sẽ không phân biệt được đâu là bản gốc, đâu là bản giả mạo nếu hacker dùng đúng chữ ký đó.
- Bạn mất quyền kiểm soát hoàn toàn ứng dụng. Hacker có thể cập nhật app độc hại lên chợ ứng dụng bên thứ ba, hoặc thậm chí đánh lừa người dùng cài đặt lại. Các nguyên nhân phổ biến gây lộ keystore:
- Đẩy file keystore lên GitHub (do quên .gitignore)
- Lưu mật khẩu keystore hoặc key alias trực tiếp trong code
- Chia sẻ nhầm keystore qua email/drive mà không mã hóa
- Dùng các công cụ CI/CD (như GitHub Actions, Jenkins) nhưng không bảo vệ biến môi trường chứa key và password
Cách giải quyết:
- Không bao giờ commit file .keystore hoặc .jks lên source control như Git. Thêm vào .gitignore ngay từ đầu.
- Không hardcode password keystore vào code hoặc script. Dùng biến môi trường để truyền vào khi build.
- Lưu trữ file keystore cẩn thận, chỉ cho phép nhóm người được ủy quyền truy cập. Nên lưu trong các công cụ quản lý bí mật như: GitHub Secrets Google Secret Manager Bitrise Secret / Jenkins Credential Ghi nhớ: Lộ keystore = mất toàn quyền kiểm soát ứng dụng. Không thể thu hồi hoặc thay đổi được keystore một khi đã phát hành. Đây là lỗi bảo mật không thể đảo ngược nên cần bảo vệ tuyệt đối.
8.Kiểm Tra Tính Toàn Vẹn Lúc Chạy
**Thực trạng : **
Khi ứng dụng Android được cài đặt lên thiết bị, hacker có thể chỉnh sửa file APK (patch/hook) hoặc thay đổi logic bên trong để thực hiện các hành vi gian lận, như bỏ qua kiểm tra đăng nhập hoặc thanh toán. Vì vậy, việc kiểm tra tính toàn vẹn của ứng dụng lúc runtime là rất cần thiết.
Giải pháp :
1.Dùng Play Integrity API hoặc SafetyNet (Google)
Chúng ta nên sử dụng Play Integrity API để xác minh rằng:
- App không bị chỉnh sửa
- App đang chạy trên thiết bị thật (không bị root hoặc máy ảo)
- App được cài từ Play Store
Cách hoạt động của Integrity API sẽ là thiết bị gửi yêu cầu thông qua Google Play Services để lấy một token bảo mật. Trong quá trình này, hệ thống sẽ tự động kiểm tra tính toàn vẹn của ứng dụng, như: app có bị sửa đổi không, thiết bị có bị root, là máy ảo, hay bootloader bị mở khóa không. Token này sau đó được gửi về server để xác minh với Google, từ đó xác định ứng dụng có đang chạy trong môi trường an toàn hay không.
9. Xác thực hoặc phân quyền không an toàn
Thực trạng: Nhiều ứng dụng chỉ thực hiện xác thực (authentication) và phân quyền (authorization) ở phía client — như kiểm tra token, vai trò người dùng hoặc trạng thái đăng nhập mà không xác minh lại trên server. Điều này khiến kẻ tấn công dễ dàng giả mạo hoặc chiếm quyền truy cập bằng cách:
- Gửi lại token cũ hoặc giả mạo.
- Sửa đổi dữ liệu gửi lên để lấy thông tin người khác.
- Truy cập API dù không có quyền (ví dụ: tài khoản thường gọi API quản trị).
Giải pháp:
1.Xác thực an toàn (Authentication) Luôn xác thực phía server, không tin tưởng logic client.
- Token ngắn hạn (JWT): thời gian sống ngắn, tự động làm mới qua refresh_token.
- Multi-Factor Authentication (MFA): dùng OTP, SMS hoặc email nếu cần bảo mật cao.
- Thu hồi token khi đăng xuất hoặc khi thiết bị bị nghi ngờ.
2.Phân quyền chặt chẽ (Authorization)
- Trên mọi API nhạy cảm, kiểm tra vai trò hoặc quyền từ token phía server.
- Không cho phép người dùng tự cung cấp userId, role, hoặc permission.
- Dùng middleware hoặc interceptor kiểm tra quyền trên tất cả request.
Dưới đây tôi có một ví dụ về việc không phân quyền chặt chẽ và xác thực lại với server khi truy xuất học liệu dẫn đến hệ quả là lộ lọt các học liệu trả phí
Kết Luận
Top 10 lỗ hổng bảo mật phổ biến trên ứng dụng di động theo OWASP Mobile Top 10 (2024):
M1: Improper Credential Usage – Sử dụng thông tin xác thực không đúng cách
M2: Inadequate Supply Chain Security – Bảo mật chuỗi cung ứng không đầy đủ (thư viện bên thứ ba, SDK không an toàn)
M3: Insecure Authentication/Authorization – Xác thực hoặc phân quyền không an toàn
M4: Insufficient Input/Output Validation – Kiểm tra dữ liệu vào/ra không đầy đủ (có thể dẫn đến tấn công injection)
M5: Insecure Communication – Truyền dữ liệu không an toàn (thiếu HTTPS, certificate pinning, v.v.)
M6: Inadequate Privacy Controls – Kiểm soát quyền riêng tư không đầy đủ (thu thập dữ liệu không cần thiết hoặc để lộ dữ liệu nhạy cảm)
M7: Insufficient Binary Protections – Thiếu bảo vệ mã nhị phân (dễ bị phân tích ngược, giả mạo mã)
M8: Security Misconfiguration – Cấu hình bảo mật sai hoặc thiếu (debug bật, lỗi cấp quyền)
M9: Insecure Data Storage – Lưu trữ dữ liệu không an toàn (ghi vào file không mã hóa, dùng SharedPreferences sai cách)
M10: Insufficient Cryptography – Mã hóa không an toàn hoặc sai cách sử dụng thuật toán mã hóa
Việc bảo vệ ứng dụng Android khỏi các lỗ hổng bảo mật phổ biến không chỉ giúp bảo vệ dữ liệu người dùng mà còn nâng cao uy tín và sự tin cậy của sản phẩm trên thị trường. Trong bài viết này, chúng ta đã điểm qua những lỗ hổng thường gặp như lưu trữ dữ liệu không an toàn, truyền dữ liệu không mã hóa, log thông tin nhạy cảm, hardcoded keys, mã nguồn dễ bị dịch ngược, WebView không kiểm soát, lộ keystore và kiểm tra tính toàn vẹn lúc chạy.
Các biện pháp bảo mật được đề xuất đều hướng tới việc giảm thiểu tối đa rủi ro do những điểm yếu này gây ra, đồng thời góp phần bảo vệ ứng dụng khỏi những kỹ thuật tấn công phổ biến hiện nay.