On December 3, 2015, Apple open-sourced Swift alongside a brand-new dependency manager: Swift Package Manager. A decade later, SPM is the default way iOS, macOS, and server-side Swift teams pull in third-party code, integrated directly into Xcode and the swift build toolchain. Yet swift package manager security still gets a fraction of the scrutiny applied to npm or PyPI hardening — even though SPM resolves most packages straight from git repositories with no centralized registry vetting who publishes what. A single mutable git tag, a branch reference instead of a pinned version, or a stale Package.resolved file is enough for an attacker to swap in malicious code that ships straight into a production app. This post walks through how SPM actually resolves dependencies, where the trust gaps live, and what concrete steps close them.
What is Swift Package Manager security and why does dependency resolution matter?
Swift Package Manager security matters because SPM treats git history, not a signed artifact, as the unit of trust — and git history is editable. Unlike npm's registry, which stores immutable tarballs once published, or Maven Central, which requires GPG-signed uploads, SPM's default source is a plain git URL declared in a Package.swift manifest. When you run swift package resolve, the tool clones the repository, walks its tags to find versions matching your semantic version constraints, and checks out the commit associated with the highest matching tag. If that tag ever gets moved — something git allows by design — the "same version" can silently point to different code tomorrow. Apple's own Package Manager documentation describes tags as the primary versioning mechanism, but nothing in the protocol prevents a compromised maintainer account, or a compromised CI token with push access, from force-moving a tag like 2.4.1 to a new commit after the fact.
How does SPM dependency resolution actually work?
SPM dependency resolution works by building a full dependency graph from every Package.swift manifest in your tree and picking the newest version that satisfies every constraint simultaneously — a process Apple calls version resolution, similar in spirit to Cargo's or Bundler's resolvers. Each package can declare requirements like .upToNextMajor(from: "1.2.0"), .exact("3.0.1"), or branch("main"). The resolver fetches manifests transitively — so a single top-level dependency can pull in dozens of second- and third-order packages you never explicitly reviewed. Because manifests are executable Swift code, not static JSON, resolution also means SPM evaluates arbitrary logic from every dependency and transitive dependency in your graph before a single line of your own app code compiles. That combination — executable manifests plus git-tag versioning plus deep transitive graphs — is exactly the profile that made npm's install scripts and left-pad-style incidents so damaging in the JavaScript ecosystem, and SPM has not meaningfully solved either problem.
What is Package.resolved integrity and why does it matter?
Package.resolved integrity means the checked-in Package.resolved file — which pins every dependency to an exact revision hash and version — actually matches what gets fetched at build time, and in practice that guarantee is weaker than most teams assume. Xcode 13 and Swift 5.6, shipped in 2021 and March 2022 respectively, introduced the version-2 Package.resolved schema, adding fields like originHash to better detect when a package's source has drifted. But the file only protects you if it's actually committed, actually reviewed in pull requests, and never regenerated carelessly. Teams that run swift package update locally and let CI silently re-resolve on every build lose the protection entirely, because CI ends up trusting whatever the upstream git tag currently points to rather than the hash your team last approved. A Package.resolved file that isn't diffed in code review is functionally decorative — it records history, but enforces nothing.
How does swift dependency pinning protect against supply chain attacks?
Swift dependency pinning protects against supply chain attacks by forcing every build to use an exact, previously audited commit hash instead of a mutable version label, closing the window where a tag can be quietly repointed. The practical steps are straightforward but frequently skipped: pin to .exact() versions rather than range constraints for anything security-sensitive, commit Package.resolved to source control, block CI from running unattended swift package update, and require a human reviewer to approve any diff that changes a resolved commit hash. Teams should also treat branch() and revision() requirements pointing at moving targets as a red flag in code review — a 2023 GitHub Octoverse supply chain report noted that dependency-related alerts across open-source ecosystems rose sharply year over year, and unpinned branch dependencies are one of the most common root causes because they bypass tag-based review entirely. Pinning doesn't eliminate the need to trust upstream maintainers, but it converts an ongoing, invisible trust relationship into a one-time, auditable decision.
What real-world incidents have exploited Swift Package Manager weaknesses?
Real-world incidents exploiting Swift Package Manager weaknesses have mostly mirrored the dependency-confusion and typosquatting attacks documented across other ecosystems since security researcher Alex Birsan's 2021 dependency confusion disclosure, which showed that private package names could be shadowed by public packages of the same name with a higher version number. SPM's lack of a single, namespaced registry — even after the Swift Package Registry specification (SE-0292) was accepted in 2021 to standardize registry servers — means most teams still resolve packages by raw GitHub URL, where a typosquatted fork (github.com/faceb00k/swift-sdk instead of the legitimate org) is trivially easy to publish and hard to catch in a quick manifest review. The Swift Package Index, the community's closest thing to a curated catalog, has grown to list several thousand packages, but indexing there is opt-in and provides no cryptographic guarantee about ongoing maintainer integrity — a package can be legitimate at index time and compromised months later through an account takeover.
How Safeguard Helps
Safeguard closes the gaps that manual Package.resolved review and ad hoc tag-pinning policies can't cover at scale. Safeguard continuously monitors every Swift package in your dependency graph — direct and transitive — for maintainer changes, force-moved or rewritten git tags, and newly introduced build-time scripts, then flags any diff to a resolved commit hash before it merges. Instead of trusting that CI resolved the same code your team last reviewed, Safeguard verifies Package.resolved integrity on every build and alerts when a pinned revision no longer matches what upstream git currently serves. It also cross-references package sources against known typosquatting patterns and abandoned-maintainer signals, so a look-alike fork or a hijacked account gets caught before it reaches a pull request. For teams running Swift across iOS, macOS, and server-side workloads, that turns swift package manager security from a manual, easy-to-skip checklist item into a continuously enforced, auditable control — giving security and engineering teams the same level of software supply chain assurance for Swift that they already expect from more mature registries like npm and Maven Central.