logo

Database

Supply Chain Attack - Lock Files

Need

Reproducible and verifiable dependency resolution with a committed mix.lock

Context

• Usage of Elixir 1.15+ with Mix for building application services

• Usage of Hex packages as dependencies

Description

1. Non compliant code

defmodule MyApp.MixProject do
  use Mix.Project

  def project do
    [app: :my_app, version: "1.0.0", elixir: "~> 1.15", deps: deps()]
  end

  defp deps do...

The `mix.exs` file below declares Hex dependencies with open requirements such as `">= 0.0.0"`, and the project does not commit a `mix.lock` file, for example because it is listed in `.gitignore`. Without the lock file, every `mix deps.get` resolves the requirements again against the current state of Hex. Each developer machine, CI run and release build can install a different version of every dependency, including versions published after the code was reviewed. If a maintainer account is compromised, or a malicious version of a transitive dependency is published, the next build installs and runs it, since Mix compiles and executes dependency code during the build. The lock file also records a checksum of every package. Without it, nothing detects that the content of a version has changed.

2. Steps

• Commit `mix.lock` to version control next to every `mix.exs` that declares Hex dependencies, and remove it from `.gitignore`.

• Constrain dependencies with the `~>` operator instead of open requirements such as `">= 0.0.0"`.

• Run `mix deps.get --check-locked` in CI so builds fail when the lock file is out of date.

• Update dependencies deliberately with `mix deps.update` and review the changes to `mix.lock`.

• Run `mix hex.audit` regularly to detect retired packages.

3. Secure code example

defmodule MyApp.MixProject do
  use Mix.Project

  def project do
    [app: :my_app, version: "1.0.0", elixir: "~> 1.15", deps: deps()]
  end

  defp deps do...

The corrected project constrains each dependency with the `~>` operator, which accepts compatible updates within a major version, and commits the `mix.lock` file generated by `mix deps.get` next to `mix.exs`. `mix.lock` pins the exact version and the checksum of every direct and transitive dependency, so every machine and every build installs the same, reviewed code, and Mix refuses a package whose content no longer matches its checksum. Updates happen deliberately with `mix deps.update`, and the resulting change to the lock file is reviewed like any other code. In CI, `mix deps.get --check-locked` fails the build when `mix.exs` and `mix.lock` are out of sync, and `mix hex.audit` reports retired packages.