Skip to content

kotlin-security

Secure-by-default patterns for Kotlin JVM and Android. Load for security, auth, injection, deserialization, WebView, content providers.

From plugin
vexjoy-agent
413198 skills198 agents10 commands86 hooks
Install
$ npx -y skills add notque/vexjoy-agent --agent claude-code

How it fires

How this agent gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.

Context preview

The summary Claude sees to decide when to auto-load this agent.

Secure-by-default patterns for Kotlin JVM and Android. Load for security, auth, injection, deserialization, WebView, content providers.

Agent definition

kotlin-security.md

Kotlin Secure Implementation Patterns

Secure-by-default patterns for Kotlin JVM and Android. Load for security, auth, injection, deserialization, WebView, content providers.

---

Disable Jackson Default Typing

No default typing. Use explicit `@JsonTypeInfo` with closed subtype allowlist when polymorphic deser is needed.

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule

// Correct: no default typing, Kotlin module for data class support
val mapper = ObjectMapper().registerKotlinModule()

// When polymorphic deserialization is needed, use sealed classes
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes(
    JsonSubTypes.Type(Circle::class, name = "circle"),
    JsonSubTypes.Type(Rectangle::class, name = "rectangle"),
)
sealed interface Shape

data class Circle(val radius: Double) : Shape
data class Rectangle(val width: Double, val height: Double) : Shape

**Why**: `enableDefaultTyping()` lets attackers specify deserialized class. Known RCE gadget chains (C3P0, Spring). CVE-2021-44228, CVE-2022-22965.

**Detection**:

rg -n 'enableDefaultTyping|activateDefaultTyping|Id\.CLASS' . --type kotlin
rg -n 'JsonTypeInfo' . --type kotlin

---

Validate Android Intent Extras

Explicit intents for internal communication. Validate all extras from implicit intents/deep links.

// Correct: explicit intent for internal navigation
val intent = Intent(context, TargetActivity::class.java).apply {
    putExtra("orderId", orderId)
}
startActivity(intent)

// Correct: validate extras from external sources
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // Validate intent source when receiving from external apps
    val orderId = intent.getStringExtra("orderId")
        ?: return finish()  // Missing required extra

    // Validate format before use
    if (!orderId.matches(Regex("^[a-f0-9-]{36}$"))) {
        return finish()  // Invalid format
    }

    loadOrder(orderId)
}

**Why**: Implicit intents deliver attacker-controlled data — unexpected types, missing values, malicious strings.

**Detection**:

rg -n 'getStringExtra|getIntExtra|getParcelableExtra' . --type kotlin
rg -n 'Intent\(.*ACTION' . --type kotlin

---

Configure WebView Security Defaults

Disable JS by default. Enable only for trusted content with URL allowlists.

import android.webkit.WebView
import android.webkit.WebViewClient

// Correct: secure WebView configuration
webView.apply {
    settings.javaScriptEnabled = false  // Default; enable only when needed
    settings.allowFileAccess = false
    settings.allowContentAccess = false
    settings.domStorageEnabled = false

    // Override URL loading to enforce allowlist
    webViewClient = object : WebViewClient() {
        private val allowedHosts = setOf("app.example.com", "help.example.com")

        override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
            return request.url.host !in allowedHosts  // Block navigation to unknown hosts
        }
    }
}

// When JavaScript IS needed for trusted content
webView.settings.javaScriptEnabled = true
// NEVER add JavaScript interfaces that expose sensitive operations
// webView.addJavascriptInterface(...)  // Avoid unless strictly necessary

**Why**: Unrestricted JS + navigation = arbitrary code execution. `addJavascriptInterface` exposes Kotlin methods. `allowFileAccess` enables local file reads.

**Detection**:

rg -n 'javaScriptEnabled\s*=\s*true|addJavascriptInterface' . --type kotlin
rg -n 'allowFileAccess\s*=\s*true|allowContentAccess\s*=\s*true' . --type kotlin

---

Prevent Content Provider Path Traversal

Validate paths with `canonicalFile` + `startsWith` containment check.

import android.content.ContentProvider
import android.os.ParcelFileDescriptor
import java.io.File

class SecureFileProvider : ContentProvider() {
    private val baseDir by lazy {
        File(context!!.filesDir, "shared").also { it.mkdirs() }
    }

    override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
        val requestedPath = uri.lastPathSegment
            ?: throw SecurityException("missing path")

        // Resolve and verify containment
        val target = File(baseDir, requestedPath).canonicalFile
        if (!target.path.startsWith(baseDir.canonicalPath + File.separator)) {
            throw SecurityException("path traversal attempt: $requestedPath")
        }

        if (!target.exists()) {
            throw FileNotFoundException("file not found")
        }

        return ParcelFileDescriptor.open(target, ParcelFileDescriptor.MODE_READ_ONLY)
    }
}

**Why**: URI path segments with `../` escape the intended directory. `canonicalFile` resolves traversal; `startsWith` enforces containment.

**Detection**:

rg -n 'openFile|ContentProvider' . --type kotlin
rg -n 'canonicalFile|canonicalPath' . --type kotlin

---

Handle Coroutine Exceptions Without Swallowing Security Failures

Structured concurrency preserves exception propagation. Never silently catch security exceptions.

import kotlinx.coroutines.*

// Correct: structured concurrency preserves exception propagation
suspend fun processSecureRequest(request: Request): Response =
    coroutineScope {  // Cancels all children if any fails
        val authResult = async { verifyAuth(request) }
        val data = async { fetchData(request) }

        // Auth failure propagates and cancels data fetch
        val user = authResult.await()
        val result = data.await()
        Response(user, result)
    }

// Correct: supervisor scope for independent operations with logging
suspend fun batchProcess(items: List<Item>) = supervisorScope {
    items.map { item ->
        async {
            try {
                processItem(item)
            } catch (e: SecurityException) {
Read more
Ships withvexjoy-agent

Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.

Get the whole plugin, auto-invoked