logo

Database

Need

Execution of external programs without shells and with validated arguments

Context

• Usage of Kotlin 1.9+ on the JVM for building web applications

• Usage of the javax.servlet API for handling HTTP requests

Description

1. Non compliant code

import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

class PingServlet : HttpServlet() {
    override fun doGet(request: HttpServletRequest, response: HttpServletResponse) {
        val host = request.getParameter("host").orEmpty()
        // The shell runs anything appended to the parameter, such as "; cat /etc/passwd"...

The `doGet` method of the servlet below builds a command line by concatenating the `host` request parameter and runs it with `Runtime.getRuntime().exec(...)` through `sh -c`. The shell parses the whole string, so metacharacters in the parameter start new commands. A request with `host=127.0.0.1;cat /etc/passwd` runs `cat` with the privileges of the application server, and payloads with `$(...)`, backticks, `|` or `&&` work the same way. The attacker can read files, install a reverse shell or move to other systems from the server. Even without `sh -c`, calling `exec` with a single string splits it on spaces, so the caller can still add arguments such as `-f` or `--output` that change what the program does.

2. Steps

• Do not pass request data to `Runtime.exec` or `ProcessBuilder` through `sh -c`, `bash -c` or `cmd /c`.

• Run the program directly with `ProcessBuilder`, giving the program name and every argument as separate elements.

• Validate every request-derived argument against a strict pattern before it reaches the command.

• Reject values that start with `-`, and place user input after a `--` separator when the program supports it.

• Prefer a JVM library over an external program when one exists, and run the service with a user that has only the permissions it needs.

3. Secure code example

import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

private val IPV4 = Regex("""^((25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(25[0-5]|2[0-4]\d|1?\d?\d)$""")

class PingServlet : HttpServlet() {
    override fun doGet(request: HttpServletRequest, response: HttpServletResponse) {...

The corrected servlet runs `ping` with `ProcessBuilder`, passing the program and each argument as separate list elements. No shell is involved, so characters such as `;`, `|` or `$(` have no special meaning. Before that, the `host` parameter must match `IPV4`, a strict pattern for dotted IPv4 addresses. Anything else, including values that start with `-` and could be read as options, is rejected with `400 Bad Request`. `redirectErrorStream(true)` merges the error output so the response never blocks on a full buffer. When the program must receive free-form input, the same approach applies: validate it against a strict format, pass it as its own argument, and place it after a `--` separator when the program supports it.