Local file inclusion
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.
References
• 123. Local file inclusion