Native code on Android, C or C++ compiled with the NDK into a shared library and called through JNI, comes with two security misconceptions that get developers into trouble. The first is that putting a secret in native code hides it, when native libraries are still reverse-engineerable. The second is forgetting that C and C++ reintroduce memory-safety bugs, buffer overflows and the like, that managed Kotlin or Java code does not have. Native code has legitimate uses, but it is not a vault and it is not safe by default. Here is what to watch when shipping native code in an Android app.
Short answer
Android native code, written in C or C++ with the NDK and called through JNI, carries security considerations managed code does not. Per OWASP MASVS, a native library is still reverse-engineerable, so it is not a hiding place for secrets, and C and C++ reintroduce memory-safety vulnerabilities like buffer overflows and use-after-free that Kotlin and Java avoid. The defenses are to keep secrets out of native code, write memory-safe native code with careful bounds and buffer handling, validate data crossing the JNI boundary as untrusted input, and keep third-party native libraries updated for known vulnerabilities. Native code is a tool with real uses, not a security feature, and it must be written and managed carefully.
What you should know
- Native code is reverse-engineerable: a library is not a hiding place for secrets.
- C and C++ are not memory-safe: buffer overflows and use-after-free are possible.
- The JNI boundary takes input: validate data passed into native code.
- Native dependencies have CVEs: keep them updated.
- Native is not a security feature: it is harder to read, not secret or safe by default.
What are the two misconceptions about native code?
That native code hides secrets and that it is safe by default. The first misconception is treating a native library as a secure place to put an API key or logic, on the assumption that compiled C is unreadable. It is harder to analyze than Java or Kotlin, but it is still reverse-engineerable: the library can be disassembled, and strings and constants extracted, so a secret in native code is recoverable, just with more effort. The second misconception is forgetting the cost of leaving managed memory: C and C++ do not have the automatic memory safety of Kotlin and Java, so native code can have buffer overflows, use-after-free, and other memory-corruption bugs, which are a classic source of serious vulnerabilities. So native code does not give you secrecy and does not give you memory safety; it gives you the burden of both, which is the opposite of how it is sometimes treated.
What are the risks?
A mix of recoverability, memory safety, the JNI boundary, and dependencies. The table lists them.
| Risk | Why it matters |
|---|---|
| Secrets in native code | Recoverable by disassembling the library |
| Memory-safety bugs | Buffer overflows and use-after-free in C/C++ |
| Unvalidated JNI input | Untrusted data crossing into native code |
| Vulnerable native dependencies | Third-party native libraries with known CVEs |
| Larger attack surface | Native code adds a harder-to-audit layer |
The recurring theme is that native code shifts you into a less forgiving environment: the secrecy you might hope for is not there, the memory safety you had in managed code is gone, and any third-party native library you include brings its own potential vulnerabilities. The JNI boundary is a specific watch point, since data passed from the managed side into native functions is input that native code must handle safely, and a memory bug triggered by crafted input is exactly the kind of serious flaw native code can introduce.
How do you use native code securely?
Keep secrets out, write memory-safe code, validate input, and update dependencies. Do not put secrets, keys, or sensitive logic in native code expecting it to be hidden, since it is recoverable; keep real secrets on your backend as you would for any client code. Write native code with memory safety in mind, bounds-checking buffers, avoiding unsafe functions, and handling allocations carefully, to prevent overflows and use-after-free, and consider memory-safe languages or modern C++ practices where possible. Treat data crossing the JNI boundary as untrusted input and validate it before using it in native code, since a crafted value can trigger a memory bug. Keep third-party native libraries updated and check them for known vulnerabilities, as you would any dependency. And minimize the native surface to what genuinely needs it. The principle is that native code is a capability to handle carefully, not a security measure to rely on.
What to watch out for
The first trap is hiding a secret in a native library, which is recoverable by disassembly; keep secrets server-side. The second is memory-safety bugs in C or C++, which are serious and absent in managed code; write defensively. The third is unvalidated input across the JNI boundary, and outdated native dependencies carrying CVEs. A pre-submission scan such as PTKD.com (https://ptkd.com) reads the compiled APK or AAB against OWASP MASVS and surfaces native libraries and the strings and secrets in them, so you can confirm no secret is hidden in native code and see which native dependencies you ship. The memory-safety and input-validation work happens in your native code.
What to take away
- Android native code via the NDK and JNI is reverse-engineerable, so it is not a hiding place for secrets, and C and C++ reintroduce memory-safety bugs that managed code avoids.
- The risks are recoverable secrets, memory-corruption bugs, unvalidated JNI input, and vulnerable third-party native libraries.
- Keep secrets out of native code, write memory-safe code, validate data crossing the JNI boundary, and keep native dependencies updated.
- Use a pre-submission scan such as PTKD.com to surface native libraries and any secrets in them, and to see which native dependencies you ship.


