logo

Database

Need

Prevention of code injection by never evaluating user input as code

Context

• Usage of Kotlin 1.9+ on the JVM for building application services

• Usage of the javax.script scripting API

Description

1. Non compliant code

import javax.script.ScriptEngineManager

fun calculate(formula: String): Any? {
    val engine = ScriptEngineManager().getEngineByName("js")
    // The formula runs as code with the privileges of the application
    return engine.eval(formula)
}

The `calculate` function below receives a formula from the user and evaluates it with a JavaScript engine obtained from `ScriptEngineManager`. `ScriptEngine.eval` executes whatever it receives as code, with the permissions of the application. A formula such as `java.lang.Runtime.getRuntime().exec("rm -rf /data")` runs an operating system command, and other payloads can read files, open network connections or modify objects of the application. Validating the input with a blacklist of words is not enough, since scripting languages offer many ways to reach the same classes. The same risk applies to any API that compiles or interprets text as code, such as Groovy shells, SpEL expressions built from user input or `Class.forName` with a user-supplied name.

2. Steps

• Never pass user input to `ScriptEngine.eval`, Groovy shells or expression languages such as SpEL.

• Parse user-supplied formulas or rules with a strict grammar and compute the result in Kotlin code.

• Map user choices to a closed set of operations with `when` or a map, instead of turning them into code.

• If scripting is unavoidable, run it in a separate, sandboxed process with no access to the application classes.

• For operating system commands, use `ProcessBuilder` with separate, validated arguments and never a shell.

3. Secure code example

private val FORMULA = Regex("""^\s*(-?\d{1,9}(?:\.\d{1,6})?)\s*([+\-*/])\s*(-?\d{1,9}(?:\.\d{1,6})?)\s*$""")

fun calculate(formula: String): Double {
    // Parse a fixed grammar instead of evaluating the input as code
    val (left, operator, right) = FORMULA.matchEntire(formula)?.destructured
        ?: throw IllegalArgumentException("Unsupported formula")
    val a = left.toDouble()
    val b = right.toDouble()...

The corrected function does not evaluate code. It accepts formulas of the form `<number> <operator> <number>`, parses them with a strict pattern and computes the result with a `when` over a closed set of operators. Anything that does not match the pattern, including function calls, property access or any other syntax, is rejected with an `IllegalArgumentException`, so user input can only ever select data, never behavior. When richer expressions are required, a dedicated expression library with a restricted grammar, such as a math parser that does not expose host classes, is safer than a general-purpose scripting engine.