logo

Database

Lack of data validation - Path Traversal

Need

Restriction of files served by an endpoint to a fixed directory

Context

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

• Usage of the Ktor server framework

• Usage of java.nio.file for filesystem access

Description

1. Non compliant code

import io.ktor.server.application.Application
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.routing
import java.io.File

private val REPORTS_DIR = File("/srv/app/reports")
...

The `/reports/{name}` route below builds a `File` from a base directory and the `name` path parameter, and returns its content with `respondText`. `File(parent, child)` does not stop the child from climbing out of the parent. A `name` whose value is `../../etc/passwd` reads a file outside the reports directory, and the same trick reaches application configuration files, private keys and credentials files. When the child is an absolute path, `Paths.get(base, child)` and `Path.resolve` discard the base entirely.

2. Steps

• Never build `File`, `Paths.get` or `Path.resolve` arguments directly from request parameters in Ktor or JAX-RS handlers.

• Accept only a single file name that matches a strict pattern, or look files up by an identifier stored in the database.

• Normalize the resolved path and confirm with `startsWith` that it stays inside the base directory, using `toRealPath()` to account for symbolic links.

• Answer with `404 Not Found` for invalid names instead of revealing why the request was rejected.

• Run the service with a filesystem user that can only read the directories it serves.

3. Secure code example

import io.ktor.http.HttpStatusCode
import io.ktor.server.application.Application
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.routing
import java.nio.file.Files
import java.nio.file.Path...

The corrected route resolves the requested name with `reportPath`, which only accepts a single file name made of letters, digits, `.`, `-` and `_` that does not start with a dot. Separators, `..` segments and absolute paths never pass. The name is then resolved against the canonical reports directory and normalized, and the result must still start with that directory. `toRealPath()` follows symbolic links, so a link planted inside the directory that points elsewhere is also rejected, and a missing file makes the route answer `404 Not Found`. Because the check works on the resolved path rather than on the raw string, encodings such as `%2e%2e%2f` or mixed separators cannot bypass it.