logo

Database

Need

Selection of executable code from a fixed allowlist instead of user-supplied paths

Context

• Usage of Elixir 1.15+ for building application services

• Usage of Plug.Router for handling HTTP requests

Description

1. Non compliant code

defmodule MyAppWeb.ExportRouter do
  use Plug.Router

  plug :match
  plug :fetch_query_params
  plug :dispatch

  get "/export" do...

The `/export` route below loads a plugin by passing the `plugin` query parameter to `Code.eval_file/1` and then calls the function it defines. `Code.eval_file/1` compiles and runs any Elixir file the process can read, with all the privileges of the application. An attacker who can place a file on the server, for example through an upload feature, a log file or a temporary directory, and then point `plugin` at it, runs arbitrary code: reading secrets from the environment, opening network connections or executing operating system commands. Even existing files, such as scripts under `priv/` or dependencies, can be run with side effects the developers never intended. `Code.require_file/1`, `Code.compile_file/1` and `Code.eval_string/1` with request data have the same effect.

2. Steps

• Never pass request data to `Code.eval_file/1`, `Code.require_file/1`, `Code.compile_file/1` or `Code.eval_string/1`.

• Compile every plugin or extension with the application and select it through an explicit map from public names to modules.

• Reject any name that is not in the allowlist instead of trying to sanitize paths.

• Package third-party extensions as reviewed dependencies instead of loading files at runtime.

• Keep uploaded and temporary files in directories that the application never reads code from.

3. Secure code example

defmodule MyAppWeb.ExportRouter do
  use Plug.Router

  @exporters %{"csv" => MyApp.Exporters.CSV, "json" => MyApp.Exporters.JSON}

  plug :match
  plug :fetch_query_params
  plug :dispatch...

The corrected route never loads code at runtime. The available exporters are ordinary modules compiled with the application, and `@exporters` maps each public name, such as `"csv"`, to its module. `Map.fetch/2` turns the `format` parameter into a module only when the name is in that map; any other value returns `:error` and the request receives `400 Bad Request`. The parameter can therefore choose among the options the developers wrote, but it can never introduce new code. When the application truly needs third-party extensions, they should be packaged as dependencies and reviewed like any other code, never loaded from paths chosen at runtime.