defmodule ComponentsElixirWeb.FileController do use ComponentsElixirWeb, :controller def show(conn, %{"filename" => encoded_filename}) do case decode_and_validate_filename(encoded_filename) do {:ok, filename} -> uploads_dir = Application.get_env(:components_elixir, :uploads_dir) file_path = Path.join([uploads_dir, "images", filename]) if File.exists?(file_path) do # Get the file's MIME type mime_type = get_mime_type(filename) conn |> put_resp_content_type(mime_type) # Cache for 1 day |> put_resp_header("cache-control", "public, max-age=86400") |> send_file(200, file_path) else conn |> put_status(:not_found) |> text("File not found") end {:error, reason} -> conn |> put_status(:bad_request) |> text(reason) end end def show_datasheet(conn, %{"filename" => encoded_filename}) do case decode_and_validate_filename(encoded_filename) do {:ok, filename} -> uploads_dir = Application.get_env(:components_elixir, :uploads_dir) file_path = Path.join([uploads_dir, "datasheets", filename]) if File.exists?(file_path) do # Get the file's MIME type mime_type = get_mime_type(filename) conn |> put_resp_content_type(mime_type) # Cache for 1 day |> put_resp_header("cache-control", "public, max-age=86400") |> put_resp_header("content-disposition", "inline; filename=\"#{filename}\"") |> send_file(200, file_path) else conn |> put_status(:not_found) |> text("File not found") end {:error, reason} -> conn |> put_status(:bad_request) |> text(reason) end end defp decode_and_validate_filename(encoded_filename) do try do # URL decode the filename decoded_filename = URI.decode(encoded_filename) # Security validation: prevent directory traversal and only allow safe characters # Allow letters, numbers, spaces, dots, dashes, underscores, parentheses, and basic punctuation if String.match?(decoded_filename, ~r/^[a-zA-Z0-9\s_\-\.\(\)\[\]]+$/) and not String.contains?(decoded_filename, "..") and not String.starts_with?(decoded_filename, "/") and not String.contains?(decoded_filename, "\\") do {:ok, decoded_filename} else {:error, "Invalid filename: contains unsafe characters"} end rescue _ -> {:error, "Invalid filename: cannot decode"} end end defp get_mime_type(filename) do case Path.extname(filename) |> String.downcase() do ".jpg" -> "image/jpeg" ".jpeg" -> "image/jpeg" ".png" -> "image/png" ".gif" -> "image/gif" ".webp" -> "image/webp" ".pdf" -> "application/pdf" _ -> "application/octet-stream" end end end