Subscribe

Kotlin null safety shrinks CWE-476. It is not a shield

An empty teal coat hook with a coral missing-coat outline, house style.

CWE-476 is NULL Pointer Dereference. I opened the MITRE CWE-476 page on 22 August 2026. The class is still a crash or a skip when code follows a pointer that is not there. Kotlin’s type system shrinks that class for Kotlin sources. A !!, a Java platform type, or an uninitialized lateinit still throws. That is CWE-476 with a smaller door, not a silver bullet.

A language feature list is not a control. The control is a typed absence, a hatch you can grep, and a Java boundary you mark. Keep the secure coding checklist next to this page. Read input validation when the missing value is a field the client omitted. Read injection when a null check is standing in for a bind.

CWE-476 names the miss

A null dereference is not a brand. It is CWE-476: the program uses a reference that holds no object. In Java the runtime name is NullPointerException. In Kotlin the same JVM exception appears when you asked for it or when a Java value arrived unmarked. Tony Hoare called the null reference a billion-dollar mistake in 2009. Kotlin’s own null safety page,, still uses that name and still says the feature is designed to reduce the risk, not to delete the class.

Three outcomes matter for a web or Android process:

  • Crash. An uncaught NPE kills the request or the activity. Availability is the loss.
  • Skip. A broad catch (NullPointerException) swallows the miss and continues. Authorization or a write then runs on a half-built object.
  • Confused default. Code substitutes an empty string or user id 0 and stores it. That is a data bug that later looks like an IDOR.

Kotlin’s win is the first outcome becoming a compile error on Kotlin types you wrote. The second and third outcomes are still yours. A language switch that keeps a catch-all around every lookup has not shrunk the class. It has hidden it.

What the compiler actually refuses

A Kotlin String cannot hold null. A String? can. The compiler refuses account.email.length when account is Account?. You get a safe call, an explicit if, the Elvis operator, or a compile error. I am citing the Kotlin null safety page for those four paths, not a conference slide.

Identifiers on this page stay Account, findAccount, and emailOf. A missing row is null at the type, not an exception you plan to catch.

data class Account(val id: Long, val email: String)

fun findAccount(rows: Map<Long, Account>, id: Long): Account? {
  return rows[id]
}

fun emailOf(rows: Map<Long, Account>, id: Long): String? {
  return findAccount(rows, id)?.email
}

fun requireEmail(rows: Map<Long, Account>, id: Long): String {
  return emailOf(rows, id)
    ?: throw NoSuchElementException("account $id")
}

emailOf is the lookup a handler should call when absence is normal. requireEmail is the named fallback when the route cannot continue without a row. The fallback throws a domain exception, not NullPointerException. A 404 or a 422 is then a mapping you wrote. A crash is not.

fun handleProfile(rows: Map<Long, Account>, rawId: String): String {
  val id = rawId.toLongOrNull() ?: return "bad id"
  val email = emailOf(rows, id) ?: return "missing"
  return escapeHtml(email)
}

toLongOrNull is the same idea on a string that might not parse. Do not call toLong() on a query parameter and catch NumberFormatException three layers up. The type already has the miss.

Typed absence versus a hatch. The compiler only covers the left column.
Kotlin source
  findAccount : Account?
  emailOf     : String?     ?. or branch
  requireEmail: String      domain throw

Java boundary
  Account!    platform type
  mark it     @Nullable / @NotNull
  or          val a: Account? = javaRepo.get(id)

Hatches
  account!!           asks for NPE
  catch (NPE)         hides CWE-476
  lateinit not set    still throws

The !! hatch still throws

The not-null assertion operator converts a nullable value to a non-null type. If the value is null, the Kotlin docs say you get an NPE. That is the hatch. I opened the “Not-null assertion operator” section on the same null safety page. The sample assigns null to String?, then calls b!!.length, and the printed exception is java.lang.NullPointerException.

// BAD: assertion that becomes CWE-476 at runtime
// val email = findAccount(rows, id)!!.email

// BAD: the SO shape. default null, then !!
// fun maybeWrite(message: String? = null) = write(message!!)

Grep !! the way you grep as on an unchecked cast. A hit in a test that builds a fixture can stay. A hit in a request path is a review. lateinit is the other hatch: a var you promise to set before first read. If a lifecycle callback never runs, you get the same exception with a different stack. Prefer constructor injection of Account over lateinit var account: Account on an Android Activity unless the framework forces the later bind.

Java still arrives unmarked

I opened the Kotlin Java-interop page the same day. Java references may be null. Kotlin therefore treats unmarked Java types as platform types. You cannot write the platform type in source. The compiler does not refuse a method call on it. The call can still fail at runtime. If you assign that value to a non-null Kotlin type and it is actually null, Kotlin throws NullPointerException.

That is why a 100 percent Kotlin module can still die after a Java library returns null. The interop page says to add an explicit type annotation, or to put nullability annotations on the Java side. JSpecify, JetBrains, and AndroidX annotations are the flavors that page lists. I am not picking a vendor kit. I am saying the boundary needs a mark.

// Java library you do not rewrite
public final class JavaAccountRepo {
  @org.jspecify.annotations.Nullable
  public Account find(long id) {
    return store.get(id);
  }
}
fun emailFromJava(repo: JavaAccountRepo, id: Long): String? {
  val account: Account? = repo.find(id)
  return account?.email
}

emailFromJava names the miss. val account = repo.find(id) with no annotation leaves a platform type. account.email then compiles and can throw. If the Java method lacks annotations, write the Account? yourself on the Kotlin side. Do not wait for a rewrite of the library.

Java can close the same hole

That is incomplete in 2026. Java still has no String? in the language. It does have annotations, Optional, and Objects.requireNonNull. Those shrink CWE-476 if you use them as types, not as afterthoughts.

import java.util.Map;
import java.util.Objects;
import java.util.Optional;

final class AccountLookup {
  private final Map<Long, Account> rows;

  AccountLookup(Map<Long, Account> rows) {
    this.rows = Objects.requireNonNull(rows, "rows");
  }

  Optional<Account> findAccount(long id) {
    return Optional.ofNullable(rows.get(id));
  }

  Optional<String> emailOf(long id) {
    return findAccount(id).map(a -> a.email);
  }
}

Optional.get() is Java’s !!. Call orElseThrow or orElse. A checker such as NullAway or the IDE’s null analysis will flag an unannotated get if you turn it on. I opened no 2026 first-party page that claims NullAway is part of the JDK. Treat it as an extra. The Optional return is the typed absence you can ship without Kotlin.

A silent catch is the same bug in both languages:

// BAD in Java or in Kotlin via a Java caller
try {
  return rows.get(id).email;
} catch (NullPointerException e) {
  return "";
}

Empty string is not a missing account. It is a confused default. emailOf returning Optional.empty() or null keeps the miss visible.

A type system does not close XSS or IDOR

Null safety is one CWE. It does not bind a query. It does not escape HTML. It does not check that account 41 belongs to the session. A Kotlin rewrite that still concatenates id into SQL has traded CWE-476 for CWE-89. A Kotlin view that prints account.email into HTML without an encoder has traded it for CWE-79. A Kotlin handler that loads findAccount(request.id) with no owner check has traded it for IDOR.

Those are ergonomics. They do not close an object-level check. Put the owner next to the load:

fun emailForOwner(
  rows: Map<Long, Account>,
  id: Long,
  sessionUserId: Long
): String? {
  if (id != sessionUserId) return null
  return emailOf(rows, id)
}

emailForOwner is the extra function the type system will not write for you. If the id comes from the client and the session is someone else, absence is the safe result. Do not return the row because findAccount compiled.

Android made Kotlin a first-class language in 2017. That date is adoption, not a CVE fix. A Jetpack Compose screen can still crash on a platform type from a Java SDK. The same screen can still put unescaped text into a WebView. Use the type system for absence. Use the sibling guides for the rest of the surface.

Prove the lookup

You are proving your own module. You are not crashing a foreign app.

  1. Call emailOf with an id that is not in rows. Expect null. A throw means someone used !! or getValue.
  2. Call requireEmail on the same miss. Expect NoSuchElementException, not NullPointerException.
  3. Call handleProfile with "nope". Expect "bad id". A crash means toLong() escaped.
  4. Grep the module for !!, catch (NullPointerException, and lateinit var.
  5. On each Java import that returns an object, write an explicit Account? or add a nullness annotation on the Java method.
fun test_email_of_missing_is_null() {
  val rows = mapOf(1L to Account(1L, "a@app.example"))
  require(emailOf(rows, 2L) == null)
  require(emailOf(rows, 1L) == "a@app.example")
}

fun test_require_email_names_the_miss() {
  val rows = emptyMap<Long, Account>()
  try {
    requireEmail(rows, 1L)
    throw AssertionError("expected miss")
  } catch (e: NoSuchElementException) {
    require(e.message == "account 1")
  }
}

A unit test that only walks the happy path does not prove CWE-476 is closed. The miss is the case. If you keep Java, write the same two tests against AccountLookup.emailOf and assert Optional.empty().

Questions we keep getting

Does Kotlin make NullPointerException impossible?

No. The official page lists explicit throw NullPointerException(), !!, uninitialized lateinit, and inconsistent constructor data. Java interop adds platform types. The class shrinks. It does not vanish.

Should we rewrite a Java service only for null safety?

Not for that reason alone. Optional, annotations, and a checker cut CWE-476 without a language move. Rewrite if the team already lives in Kotlin, or if Android is the runtime. Do not sell the rewrite as a security program.

Is Optional in Java as good as String?

It is a typed absence if callers cannot call get() without a check. Kotlin’s String? is checked by the compiler on every use. Optional is a wrapper people skip. Both beat a raw null plus a catch. Neither replaces a bind or an owner check.

Aphinya Dechalert

Aphinya Dechalert / About Author

Aphinya is a skilled technical writer with field experiences in software development, agile, and JavaScript full stack with AWS and Google cloud. She is a developer advocate and community builder, helping others navigate their journeys and careers as developers.