AutoPkg Pre/Post-Processor Security Considerations

Published June 19, 2024 / 872 words / ~5 minutes to read

This post explores how custom AutoPkg processors are potentially vulnerable when used as pre/post-processors. While one of AutoPkg’s main advantages is its secure by default design, there is a gap when dealing with custom processors used as command line arguments. It’s first important to understand how recipe overrides and trust information work. If you’re using AutoPkg, you should be using overrides. They provide a fully audited chain of trust for recipes, which is especially important when depending on public, community recipes. Even if recipes are published by a trusted, well known person, their GitHub account could become compromised and introduce malicious code upstream. As the AutoPkg wiki puts it…

But there is an element of risk in using other people’s recipes – a bad actor could create a malicious recipe, or a well-meaning admin could introduce an error into a recipe that causes unexpected issues.

A bad actor could initially share an innocuous recipe that behaves exactly as expected, and then later update the recipe to do something malicious, hoping you won’t notice the changes. Ideally, AutoPkg admins should re-audit any changed recipes they use, but when doing autopkg repo-update all might be overwhelmed with the huge number of potentially-irrelevant changes to sift through.

Overrides mostly solve that problem. Take the example below which is a snippet of an override (YAML formatted for ease of reading) for my 1Password.pkg.recipe. It contains the git and SHA-256 hash of the recipe chain at the moment in time the override was created. If at any point the recipe changes, trust verification will fail and the recipe won’t run. This safeguards against would be attackers hijacking a recipe, and also simple mistakes like syntax errors or typos. When the recipe does change, admins have an opportunity to audit the recipe again, make sure everything’s kosher, and update trust information. Notice even custom processors, like Elliot Jordan’s VersionSplitter, are included when used in one of the recipes.

ParentRecipeTrustInfo:
  non_core_processors:
    com.github.homebysix.VersionSplitter/VersionSplitter:
      git_hash: dc086969ec741e70edcf24774d50fba627732496
      path: ~/Library/AutoPkg/RecipeRepos/com.github.autopkg.homebysix-recipes/VersionSplitter/VersionSplitter.py
      sha256_hash: 121f98262a70e62f58e81f16de00f69785b5aed765ed43961319759757e2d55c
  parent_recipes:
    com.github.nstrauss.download.1Password:
      git_hash: 46cfd665bb0c5f613bbd9aaccc12f907e680b1b5
      path: ~/Library/AutoPkg/RecipeRepos/nstrauss-recipes/1Password/1Password.download.recipe
      sha256_hash: 1804eb9a6a4794d4a01e58ffac7b93e799c886f9bc315edfba4bb60f15b2de1a
    com.github.nstrauss.pkg.1Password:
      git_hash: 50fdbbc114e45e32af878275d752ca04320b7bf8
      path: ~/Library/AutoPkg/RecipeRepos/nstrauss-recipes/1Password/1Password.pkg.recipe
      sha256_hash: e720f608865349d118d0bad97a37556e4b64f8624a01ae3dc250ecc14cb0b60e

Pre/post-processors work differently though. While working on VirusTotalReporter, especially its usage as a post-processor, I realized even with a meticuously kept set of overrides, the exact bad actor scenario described above could still play out. And that’s because pre/post-processors are not included in overrides. When a custom processor is used as a command line argument, outside of a recipe, it is never evaulated for trust. Which from a recipe trust standpoint makes sense. It’s the recipe which is being evaluated, not the AutoPkg run itself. There’s no mechanism which includes an auxiliary trust store for custom processors defined outside recipes. I know some admins haven’t thought through the security implications though, if only because I certainly hadn’t until recently. There are workflows out there regularly running autopkg repo-update all which immediately afterward use post-processors from the same pull. When that happens, a high level of risk is introduced since custom processors have the power to run arbitrary code. Pre/post-processors going unchecked gives AutoPkg, and would be attackers, carte blanche. So what’s an admin to do?

  1. Submit a PR to AutoPkg which implements support for auditing and storing trust information for pre/post-processors.
  2. Modify my AutoPkg runner, a wrapper script which runs AutoPkg in a CI/CD pipeline, to check for pre/post-processor trust. For an example runner, see Gusto’s autopkg_tools.py.
  3. Vendor custom processors intended to be used as pre/post-processors.

Since the first two require significant code changes, and I’m all about path of least resistance, my recommendation is option three. Vendoring is the practice of copying a third party package or library (or processor) directly into your project. Instead of installing from remote, use a static copy. Though this means more maintenance burden, it’s also more secure and resilient to the type of attacks being discussed. For processors which will be used as pre/post-processors, copy them wholesale to a repo you own.

.
└── ~/Library/
    └── AutoPkg/
        └── RecipeRepos/
            └── com.github.nstrauss/
                └── VendoredProcessors/
                    ├── VendoredProcessors.recipe.yaml
                    ├── LastRecipeRunChecker.py
                    ├── LastRecipeRunResult.py
                    └── VirusTotalReporter.py

Include a stub recipe as well to easily reference the vendored processors on the command line. Here’s an example YAML recipe, represented by VendoredProcessors.recipe.yaml in the directory structure above.

Description: Stub recipe to be used as a reference for processors in this directory.
Identifier: com.github.nstrauss.VendoredProcessors
MinimumVersion: "2.3"
Input: {}
Process: []

AutoPkg can then be run referencing that processor knowing the code is trusted and secure.

autopkg run 1Password.pkg --post com.github.nstrauss.VendoredProcessors/VirusTotalReporter

I am doing this in my own organization for all public custom processors, even those which I wrote. Though I practice good security hygiene and consider my GitHub account well protected, everyone makes mistakes. If a public repo becomes compromised, workflows using unaudited code in pre/post-processors do as well. The irony of a community processor like VirusTotalReporter, whose entire purpose is to help detect vulnerabilities in imported packages, potentially compromising AutoPkg runs is not lost on me. In the future I would like to see AutoPkg 3.x make improvements in the security of pre/post-processors, but for now organizations with stricter requirements can look to vendoring as an added precaution.