Skip to main content
CleanStart

Knowledge Hub

What is a Package Manager?

Why Package Managers Are Critical for Security

Without a package manager, adding a single library means manually downloading it, checking for conflicts with other libraries, and repeating this for every transitive dependency—unsustainable at scale. More importantly: when a vulnerability is found in one of those dependencies, you need to know immediately which versions are affected and propagate updates across all projects. Package managers make this possible: they maintain a manifest of what you need, lock versions for reproducibility, and integrate with vulnerability scanners to tell you when you're using outdated packages.

A package manager automates installing, updating, and managing software libraries that your code depends on. Instead of manually downloading files and resolving version conflicts, you declare what you need and the package manager handles installation, dependency resolution, and consistency across your projects.

graph LR    A["Declare Dependencies<br/>requirements.txt<br/>package.json<br/>pom.xml"] --> B["Package Manager<br/>pip, npm, Maven<br/>Cargo, RubyGems"]    B --> C["Resolve<br/>Dependencies"]    C --> D["Fetch from<br/>Registry<br/>PyPI, npm, Maven<br/>Central, crates.io"]    D --> E["Lock<br/>Versions"]    E --> F["Install<br/>Locally"]    F --> G["Updated<br/>Application<br/>Ready"]

Common Package Managers by Language

Every major programming language has its own ecosystem:

Language

Package Manager

Registry

File Format

Python

pip

PyPI

requirements.txt, pyproject.toml, Pipfile

JavaScript/Node.js

npm

npmjs.com

package.json, package-lock.json

Java

Maven

Maven Central

pom.xml

Java

Gradle

Maven Central, Gradle Plugin Portal

build.gradle

Go

go mod

pkg.go.dev

go.mod, go.sum

Rust

Cargo

crates.io

Cargo.toml, Cargo.lock

C/C++

vcpkg or Conan

varies

vcpkg.json or conanfile.txt

Ruby

gem

rubygems.org

Gemfile, Gemfile.lock

.NET

NuGet

nuget.org

.csproj, packages.config

Direct Dependencies vs Transitive Dependencies

This is crucial for understanding how supply chain attacks happen.

Direct dependency: You explicitly say "I need this library."

# Python example: requirements.txtflask==2.0.0        # ← Direct: You explicitly need Flask
// Node.js example: package.json{  "dependencies": {    "express": "4.18.0"  // ← Direct: You explicitly need Express  }}

Transitive dependency: You don't directly depend on it, but one of your direct dependencies does.

Your Code has two direct dependencies: flask==2.0.0 and requests==2.28.0. Flask pulls in jinja2==3.0.0 and werkzeug==2.0.0 as transitive dependencies. Jinja2 itself depends on markupsafe==2.0.0, which is a transitive-of-transitive. Requests depends on urllib3==1.26.0 as a transitive dependency. In total, you have 1 direct Flask dependency plus 1 direct requests dependency, plus 4 transitive dependencies (jinja2, markupsafe, werkzeug, urllib3), giving you 5 packages in your app.

Why transitive dependencies matter: A vulnerability in a transitive dependency affects you too. You didn't choose it, but you run it. These are harder to track and manage. Supply chain attacks often target transitive dependencies (fewer watchers).

Lock Files: Reproducibility

When you specify a dependency, you usually specify a version range, not an exact version. The package manager then "locks" to specific versions.

Example without lock file (dangerous):

# requirements.txtflask>=2.0.0      # "Any Flask 2.0.0 or newer" # January: pip install → installs Flask 2.3.0 (latest)# June: pip install → installs Flask 2.5.0 (newer version released)# Different versions in dev vs production → BUG!

Example with lock file (safe):

# requirements.txt (what you write)flask>=2.0.0 # requirements.lock (auto-generated, check into version control)flask==2.3.0jinja2==3.1.0werkzeug==2.2.0# ... all transitive deps locked to exact versions # Any developer or server that runs "pip install -r requirements.lock"# gets the exact same versions

Lock files by language:

Language

Lock File

Python

requirements.lock or Pipfile.lock

Node.js

package-lock.json or yarn.lock

Java/Maven

pom.xml (specifies versions)

Go

go.sum

Rust

Cargo.lock

Ruby

Gemfile.lock

Best practice: Always commit lock files to version control.

# Version controlgit add package.json package-lock.jsongit add requirements.txt requirements.lockgit add go.mod go.sum # Now every checkout gets the same versions

The Dependency Tree: Understanding Your Supply Chain

Every application has a dependency tree—a graph of all packages it depends on.

Real example: A simple Flask web app

When you run pip install flask, many more packages are installed than just Flask itself. Flask==2.0.0 is the main package. It depends on jinja2==3.0.0, which in turn depends on markupsafe==2.0.0. Flask also depends on werkzeug==2.0.0, which has no sub-dependencies. Flask additionally depends on click==8.0.0 and itsdangerous==2.0.0, neither of which have sub-dependencies. In total, you get 1 Flask package plus 5 dependencies for a total of 6 packages installed.

Visualizing your dependency tree:

# Python: Show dependency treepip install pipdeptreepipdeptree # Node.js: Show dependency treenpm list # Go: Show dependency treego mod graph # Rust: Show dependency treecargo tree

Why you need to understand this: Security: A vulnerability in any of the 6 packages affects you. Size: All 6 packages end up in your container, weighing it down. Licensing: Each package has a license; you inherit the obligations. Updates: Updating Flask might require updating 5+ transitive packages. Supply chain: Any of these 6 packages could be compromised.

Why Dependencies Are a Security Concern

Your dependencies are an attack surface because: 1. Dependency confusion attack: An attacker publishes a malicious package with a popular name to PyPI. When a team member runs pip install flask, pip finds the flask package on PyPI and installs it, potentially getting the attacker's malicious version. The solution is to use private registries or clearly name packages to avoid confusion.

2. Abandoned dependency with a vulnerability: A package like A version 1.5.0 starts as safe. Over 3 years, developers abandon maintaining it. A vulnerability is discovered in version 1.5.0. Because no one maintains the package anymore, no patch is released. You're stuck running vulnerable code indefinitely.

3. Typosquatting: An attacker publishes similar-named packages hoping developers will mistype package names. The real package is requests==2.28.0, but an attacker publishes requets==2.28.0 (missing an 's'). A developer making a typo when running pip install requets accidentally installs the attacker's malicious package instead.

4. Legitimate package gets compromised: A maintainer's credentials are stolen. The attacker releases version 1.2.3 containing malicious code. Users running pip install somepackage get the compromised version. It takes days or weeks for discovery and remediation, during which attackers exploit the vulnerability.

Package Manager Examples: Installing and Checking

Python (pip)

# Install a packagepip install flask # Install from requirements filepip install -r requirements.txt # Install with lock file (reproducible)pip install -r requirements.lock # Check installed packagespip list # Check specific packagepip show flask # Update a packagepip install --upgrade flask # See dependency treepipdeptree # Freeze current environmentpip freeze > requirements.txt

Node.js (npm)

# Install a packagenpm install express # Install and save to package.jsonnpm install --save express # Install from package.jsonnpm install # Install from package-lock.json (exact versions)npm ci # Check installed packagesnpm list # Check specific packagenpm info express # Update a packagenpm update express # See dependency treenpm list --all # Lock current versionsnpm ci (uses package-lock.json)

Java (Maven)

# Define in pom.xml<dependency>    <groupId>org.apache.logging.log4j</groupId>    <artifactId>log4j-core</artifactId>    <version>2.17.0</version></dependency> # Install dependenciesmvn install # Check dependency treemvn dependency:tree # Check for updatesmvn versions:display-dependency-updates # Check for vulnerabilitiesmvn org.owasp:dependency-check-maven:check

Go (go mod)

# Add a dependencygo get github.com/gin-gonic/gin # Update all dependenciesgo get -u ./... # See dependency treego mod graph # Lock versionsgo.mod (checked into version control)go.sum (checksums verified) # Check for vulnerabilitiesgo list -json -m all | nancy sleuth

Rust (Cargo)

# Add dependencycargo add tokio # Update dependenciescargo update # See dependency treecargo tree # Check for vulnerabilitiescargo audit # Lock versions (automatic in Cargo.lock)

Real-World Example: Resolving a Vulnerable Dependency

Scenario: A security scan finds CVE-2021-3807 in electron-builder.

Step 1: Understand the dependency

npm list electron-builder # Output:# my-app@1.0.0 has electron-builder@3.0.0 as a direct dependency.# electron-builder in turn depends on app-builder-lib@4.0.0,# builder-util@4.1.0, and dotenv@5.0.0 as its transitive dependencies.# These are all pulled in automatically when you install electron-builder.

Step 2: Check the CVE

CVE-2021-3807 in electron-builderCVSS: 7.5 HIGHAffected versions: < 23.0.0Fixed in: 23.0.0+Description: Path traversal vulnerability

Step 3: Check your version

npm info electron-builder | grep latest# latest: 24.6.4 # You have 3.0.0, need to upgrade!

Step 4: Plan the upgrade

# Check what changed between 3.0.0 and 23.0.0# (this is a major version bump, likely breaking changes)npm show electron-builder@3.0.0 versionnpm show electron-builder@23.0.0 version# Review changelog at https://github.com/electron-userland/electron-builder # Test before upgrading in production!

Step 5: Upgrade

npm install electron-builder@latest # This updates package.json and package-lock.json

Step 6: Test

# Run testsnpm test # Build your appnpm run build # Verify the vulnerability is fixednpm audit | grep CVE-2021-3807# Should show: [RESOLVED]

Step 7: Commit

git add package.json package-lock.jsongit commit -m "chore: upgrade electron-builder to fix CVE-2021-3807"

What Matters

A package manager is fundamentally a tool for managing dependencies automatically, handling both direct dependencies you explicitly declare and transitive dependencies pulled in by your direct dependencies. Understanding the distinction between direct and transitive dependencies is critical because both affect your security posture. Lock files ensure reproducibility by recording the exact versions used in a working build, and you should always commit these to version control. Your entire dependency tree represents your attack surface, because every package you depend on—directly or indirectly—introduces potential vulnerabilities. Vulnerabilities cascade through dependency trees, meaning a flaw in a transitive dependency affects you just as much as a flaw in a package you directly depend on.

Next Steps

To deepen your understanding, read about what software libraries actually are and how they provide functionality. Learn about transitive dependencies to understand the dependency explosion problem that occurs when packages depend on other packages. Understand CVEs and how vulnerabilities are tracked and disclosed. Learn about SBOMs in CycloneDX format to see how to track dependencies systematically. Practice with end-to-end secure deployment to gain hands-on experience managing dependencies in real applications.

Common mistakes to avoid when working with package managers include not committing lock files to version control, which causes different versions to be installed in development versus production. Using version ranges without lock files leads to inconsistent builds across machines and time. Ignoring transitive dependencies means you miss vulnerabilities hiding deep in your dependency tree. Upgrading dependencies without testing introduces production breaks that could have been caught in development. Installing from public registries without verification exposes you to supply chain attacks, malicious packages, and typosquatting attacks.