Insecure functionality - File Creation
Need
Creation of uploaded files only with validated names inside a fixed directory
Context
• Usage of Elixir 1.16+ for building application services
• Usage of Plug.Router and Plug.Upload for file uploads
Description
1. Non compliant code
defmodule MyAppWeb.UploadRouter do
use Plug.Router
plug :match
plug Plug.Parsers, parsers: [:multipart]
plug :dispatch
post "/upload" do...The `/upload` route below writes the uploaded content to `"uploads/" <> filename`, where `filename` comes from the multipart form, with `File.write!/2`. The caller chooses the name and the location of the new file. A name such as `../priv/static/shell.html` places an attacker-controlled page on the public site, `../config/runtime.exs` overwrites the configuration read at the next restart, and paths into other users' folders replace their files. `File.write!/2` also truncates any existing file with that name, so the same request can destroy data. `File.copy!/2` with a destination taken from the request has the same problem.
2. Steps
• Never build the destination of `File.write/2`, `File.write!/2`, `File.cp/3` or `File.copy!/2` from request data without validation.
• Accept only a single file name that matches a strict pattern, or generate the stored name on the server.
• Resolve the name with `Path.safe_relative/2` against a fixed upload directory that is not served as code or static content.
• Refuse to overwrite existing files, and limit the size of uploads in `Plug.Parsers`.
• Run the application with a filesystem user that can only write to the upload directory.
3. Secure code example
defmodule MyAppWeb.UploadRouter do
use Plug.Router
@uploads_dir "uploads"
@file_name ~r/\A[A-Za-z0-9_-][A-Za-z0-9._-]{0,99}\z/
plug :match
plug Plug.Parsers, parsers: [:multipart]...The corrected route accepts only a single file name made of letters, digits, `.`, `-` and `_`, which cannot start with a dot, so separators, `..` segments, absolute paths and hidden files such as `.htaccess` never pass. The name is resolved with `Path.safe_relative/2` against the uploads directory, which also rejects symbolic links that lead outside it. The file is created with `File.cp/3` and an `on_conflict` callback that returns `false`, so an existing file is never overwritten; the request receives `409 Conflict` instead. The uploads directory must not be served as code or static content. A stronger variant is to ignore the client name entirely, store the file under a random identifier generated by the server, and keep the original name as metadata in the database.
References
• 270. Insecure functionality - File Creation