Product

Overview

How it works Watch it work All 35 features Screenshots How ForgeRepo™ is secured

Keep bad packages out

Malware scanning Typosquat detection Dependency confusion protection Cooling off new releases Allow lists and block lists

When something goes wrong

Kill switch Who has it Lockdown and degraded modes Vulnerability monitoring

Developers and approvals

Requests and auto approve Check, walk and review Waivers Dry run
Formats

Languages

Private npm registry PyPI proxy and private index Private NuGet feed and proxy Maven repository proxy RubyGems mirror Composer and Packagist proxy

Apple, containers and Linux

CocoaPods CDN mirror Swift package registry Docker registry mirror RPM mirror for dnf and yum APT mirror for Debian and Ubuntu

Use it as

An npm firewall Artifacts, SBOMs and lifecycle More than one node SSO, roles and allow lists
Learn

Guides

Learn secure development Secure coding Secure pipelines Unsafe vs safe Security and development

Supply chain

Supply chain attacks, 2016 to 2026 Software supply chain security
Compare Side by side, all four ForgeRepo™ vs JFrog ForgeRepo™ vs Sonatype ForgeRepo™ vs Cloudsmith
Docs

Start

Getting started Download All documentation Questions

For developers

npm setup pip setup docker login Push a Docker image CI basics

For administrators

First setup Rules and their order Six incidents, walked through Backup and upgrade
Pricing Install it

Visual gallery

Unsafe vs safe, side by side

The mistakes that keep showing up in incident reports, each drawn and written next to a safer way to do the same job. Short enough to paste into a pull request comment or a team chat. For the reasoning behind each one, follow the links into the full guides.

How to read the gallery

Every pair has an Unsafe side, marked with a cross and a dashed border, and a Safer side, marked with a check and a solid border. The words and shapes carry the meaning, the red and green only repeat it. "Safer" is deliberate: each fix removes a specific risk, not every risk.

Most application bugs are data crossing into something that interprets it. Injection is covered in depth in secure coding; here are three more that reach production often.

Unsafe: a SQL query built by joining strings, where a quote in the input changes the query and returns every rowUNSAFE"...WHERE name = '" + input + "'"input: x' OR '1'='1every rowinput became part of the queryThe quote in the input rewrites the WHERE clause
Safer: a parameterized query where the same input is sent as a value and matches nothingSAFER"...WHERE name = %s", (input,)value: x' OR '1'='10 rowssent separately, only ever dataThe value travels apart from the query text

Reading a file the user names

UnsafePython (Flask)
@app.get("/files")
def download():
    name = request.args["name"]
    return send_file(os.path.join("/srv/reports", name))
# ?name=../../etc/passwd
# ?name=/etc/passwd   (join drops the base for absolute paths)

Path traversal. os.path.join is not a security check.

SaferPython (Flask)
from flask import send_from_directory

@app.get("/files")
@login_required
def download():
    name = request.args.get("name", "")
    # refuses any path that resolves outside the folder
    return send_from_directory("/srv/reports", name)

Use the framework helper that joins safely, and still check the user may see that file.

Fetching a URL for the user

UnsafeJavaScript (Node)
app.get("/preview", async (req, res) => {
  const r = await fetch(req.query.url);
  res.send(await r.text());
});
// ?url=http://169.254.169.254/latest/meta-data/iam/
// ?url=http://localhost:8080/admin

Server side request forgery, now part of A01:2025. The server reaches places the user cannot, such as cloud metadata and internal admin pages.

SaferJavaScript (Node)
const ALLOWED = new Set(["images.example.com", "cdn.partner.example"]);

app.get("/preview", async (req, res) => {
  let u;
  try { u = new URL(String(req.query.url)); } catch { return res.sendStatus(400); }
  if (u.protocol !== "https:" || !ALLOWED.has(u.hostname)) return res.sendStatus(400);
  const r = await fetch(u, { redirect: "error", signal: AbortSignal.timeout(5000) });
  res.type("text/plain").send((await r.text()).slice(0, 100_000));
});

An allow list of hosts, HTTPS only, no redirects, a timeout and a size cap. Block metadata addresses at the network too.

Reading data a client sent

UnsafePython
import pickle

@app.post("/import")
def import_order():
    order = pickle.loads(request.get_data())   # runs code on load
    save(order)
    return "", 204

Insecure deserialization. Unpickling attacker data is remote code execution. The same goes for Java native serialization and .NET BinaryFormatter.

SaferPython (pydantic)
from pydantic import BaseModel, Field

class Order(BaseModel):
    sku: str = Field(pattern=r"^[A-Z0-9-]{3,32}$")
    qty: int = Field(ge=1, le=1000)

@app.post("/import")
def import_order():
    order = Order.model_validate_json(request.get_data())
    save(order)
    return "", 204

A data only format like JSON, parsed into a strict schema. Nothing in the input can run.

The package you install runs with your permissions, in your build and on your laptop. See dependency hygiene and the supply chain attack timeline.

Unsafe: a build pulls packages straight from the public registry, including typosquats, malware and brand new releasesUNSAFEpublicregistryCI buildnpm installtyposquatmalware2 hours oldAnything published is one install away from the build
Safer: packages pass a registry firewall that checks age, scans them and applies an allow list, and blocks a bad oneSAFERpublicregistryCI buildnpm installregistry firewallagescannedallowedOnly checked, approved versions reach the build

Declaring dependencies

Unsafepackage.json
{
  "dependencies": {
    "acme-billing-utils": "^1.0.0",
    "express": "*",
    "left-pad": "latest"
  }
}

An unscoped internal name that anyone can publish on the public registry (dependency confusion), and ranges that accept any future release.

Saferpackage.json
{
  "dependencies": {
    "@acme/billing-utils": "1.4.2",
    "express": "5.1.0"
  }
}

Internal packages under a scope you own, served from your registry, exact versions for the app, and the lockfile committed. One dependency fewer, too.

Pointing the client at a registry

Unsafe.npmrc (committed)
# a real, publish capable token, committed to git
//registry.npmjs.org/:_authToken=npm_Qx81kTz7...
# no registry line: installs come straight from the internet

A publish capable token in the repository, and no gate between a new release and the build.

Safer.npmrc (committed)
registry=https://packages.example.com/
//packages.example.com/:_authToken=${NPM_TOKEN}
# where your packages allow it
ignore-scripts=true

The token comes from the environment, installs go through a registry firewall, and install scripts do not run by default.

A ForgeRepo™ instance at packages.example.com would hold new versions for a cooling off period, scan them, keep your internal names from ever resolving publicly, and let you pull a bad release from every consumer at once.

Build systems run untrusted code with trusted credentials. The full treatment, per platform, is in CI/CD pipeline security.

Unsafe: a workflow uses a third party action by a mutable version tag that an attacker moves to a malicious commitUNSAFEuses: org/setup-tool@v4a413f1c07e66v4 (was)v4 nowretaggedA tag can be moved to any commit, even a malicious one
Safer: a workflow pins a third party action to a full commit SHA, so moving a tag changes nothingSAFERuses: org/setup-tool@3f1c9e…a42b # v4.2.1a413f1c07d90pinnedupdates arrive as reviewed pull requestsA full commit SHA can only ever mean one thing

A pull request workflow

UnsafeGitHub Actions
on: pull_request_target
permissions: write-all
jobs:
  ci:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v7
        with: { ref: "${{ github.event.pull_request.head.sha }}" }
      - uses: some-org/lint-action@main
      - run: npm install && npm test

Fork code, a write token, secrets, a persistent machine and an action that changes with every push to its main branch.

SaferGitHub Actions
on: pull_request
permissions:
  contents: read
jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with: { persist-credentials: false }
      - uses: some-org/lint-action@5c7c2a1f0b9e4d3a8f6e2b1c0d9e8f7a6b5c4d3e # v2.3.1
      - run: npm ci && npm test

No secrets for untrusted code, a read only token, a fresh hosted runner, pinned actions and lockfile installs. (The lint action SHA is an illustration.)

A Jenkins pipeline

UnsafeJenkinsfile
node {                                   // any executor, controller included
  checkout scm
  sh "npm install"
  sh "curl -sL https://get.example.dev | sh"
  sh "deploy --token ${env.DEPLOY_TOKEN}"  // Groovy interpolation
}

Runs where the credentials live, pipes the internet into a shell, and leaks the token into the command line.

SaferJenkinsfile
pipeline {
  agent { kubernetes { yamlFile 'ci/pod.yaml' } }  // fresh pod per build
  stages {
    stage('Build') { steps { sh 'npm ci' } }
    stage('Deploy') {
      when { branch 'main' }
      steps {
        withCredentials([string(credentialsId: 'deploy', variable: 'T')]) {
          sh 'DEPLOY_TOKEN="$T" deploy'   // single quotes: shell reads it
        }
      }
    }
  }
}

An ephemeral agent, a lockfile install, deploys only from main, and a secret handed to the tool through its environment, never through Groovy or the command line.

Secrets leak through code, logs, images and chat. What you do in the first hour after a leak matters more than how you clean the history.

Unsafe: a long lived cloud access key pasted into a CI job and printed into the job logUNSAFEdeploy:env:AWS_KEY: AKIA2E…run: ./ship.shset -xjob log+ KEY=AKIA2E…stored for yearsprinted in the logA long lived cloud key sits in the job and ends up in the log
Safer: the CI job exchanges a short lived OIDC identity token for a cloud role, with no stored secretSAFERCI jobno secretOIDC tokensigned by CIrepo, branch,environmentcloud roledeploy-prodexpires in minutestrust tied to one repoThe job trades a signed identity token for short lived access

After a key was committed

UnsafeShell
git rm --cached .env
git commit -m "remove secrets"
git push
# the key is still in history, in every clone and fork,
# possibly already scraped, and still valid

Bots scan public pushes for keys within minutes. Removing the file does not un-leak it.

SaferRunbook
# 1. revoke the key at the provider now: treat it as public
# 2. issue a new key straight into the secret manager
# 3. check the provider's logs for use of the old key
# 4. then tidy up and prevent a repeat
echo ".env" >> .gitignore
git rm --cached .env && git commit -m "stop tracking .env"
# turn on secret scanning for pushes and pull requests

Rotate first, investigate second, clean up third. Rewriting history is optional once the key is dead.

Kubernetes secrets in the repository

UnsafeKubernetes
apiVersion: v1
kind: Secret
metadata: { name: db }
data:
  password: U3VtbWVyMjAyNiE=   # base64 is encoding, not encryption

Anyone who can read the repository can decode it in one command.

SaferKubernetes (External Secrets)
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: db }
spec:
  secretStoreRef: { name: vault, kind: ClusterSecretStore }
  target: { name: db }
  data:
    - secretKey: password
      remoteRef: { key: prod/db, property: password }

The repository holds a reference; the value lives in Vault or a cloud secret manager and is synced into the cluster.

A container is only as trustworthy as its base image, and only as contained as the privileges you give it.

Unsafe: a Dockerfile using the latest tag, npm install and the default root userUNSAFEFROM node:latestCOPY . .RUN npm install# no USER lineCMD node app.jscontainerrootchanges without noticeruns as rootA moving base image, loose installs, and root inside
Safer: a Dockerfile pinned to an image digest, installing from the lockfile and running as a non root userSAFERFROM node:24-slim@sha256:…RUN npm ci --omit=devCOPY --chown=node . .USER nodeCMD ["node","app.js"]containeruid 1000, read onlysame bytes every buildno rootPinned by digest, built from the lockfile, runs as a normal user

The Dockerfile

UnsafeDockerfile
FROM node:latest
WORKDIR /app
# copies .git, .env and everything else
COPY . .
RUN npm install
EXPOSE 3000
# runs as root, with dev dependencies
CMD npm start

A base that changes under you, secrets copied in, loose installs, a big attack surface, and root.

SaferDockerfile
# syntax=docker/dockerfile:1
FROM node:24-slim@sha256:<digest> AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev

FROM node:24-slim@sha256:<digest>
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build --chown=node:node /app/dist ./dist
COPY --from=build --chown=node:node /app/node_modules ./node_modules
USER node
CMD ["node", "dist/server.js"]

Pinned by digest, lockfile install, a .dockerignore for .git and .env, only runtime files in the final stage, and a non root user.

Running it

UnsafeShell
docker run -d --privileged \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -p 8080:8080 app:latest

Privileged plus the Docker socket is root on the host, and the port listens on every interface.

SaferShell
docker run -d --read-only --tmpfs /tmp \
  --cap-drop ALL --security-opt no-new-privileges \
  --user 10001:10001 --memory 512m --pids-limit 200 \
  -p 127.0.0.1:8080:8080 \
  registry.example.com/app@sha256:4d1f...

Read only, no capabilities, no privilege escalation, resource limits, and only reachable through the local reverse proxy.

Further reading: OWASP Docker Security Cheat Sheet and Docker's building best practices.

Infrastructure as code is code: review it, scan it, and give it safe defaults. Misconfiguration is A02 in the OWASP Top 10:2025.

Unsafe: a server with SSH open to the whole internet on a public addressUNSAFEinternetport 22 open0.0.0.0/0scanned within minutespublic IPEvery scanner on the internet can knock on the door
Safer: a server in a private subnet with no inbound port, reached by administrators through a single sign on session brokerSAFERinternetprivate subnetSSO sessionno inbound portevery session loggedNo open port; admins arrive through a logged, brokered session

Admin access to a server

UnsafeTerraform (AWS)
resource "aws_security_group_rule" "ssh" {
  type              = "ingress"
  from_port         = 22
  to_port           = 22
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.app.id
}

SSH open to the world, on a machine that probably also allows the old metadata service.

SaferTerraform (AWS)
# no ingress rule for 22 at all: admins use SSM Session Manager
resource "aws_instance" "app" {
  ami                  = var.ami_id
  instance_type        = "t3.small"
  subnet_id            = aws_subnet.private.id
  iam_instance_profile = aws_iam_instance_profile.ssm.name
  metadata_options {
    http_tokens = "required"   # IMDSv2 only
  }
}

Private subnet, no open port, sessions through IAM with an audit trail, and IMDSv2 so an SSRF cannot simply read credentials.

A storage bucket

UnsafeTerraform (AWS)
resource "aws_s3_bucket_acl" "reports" {
  bucket = aws_s3_bucket.reports.id
  acl    = "public-read"     # "so the partner can download it"
}

Every object, current and future, readable by anyone with the bucket name.

SaferTerraform (AWS)
resource "aws_s3_bucket_public_access_block" "reports" {
  bucket                  = aws_s3_bucket.reports.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}
# share single files with presigned URLs that expire

Public access blocked at the bucket, and at the account level too. Sharing is per file and time limited.

A Kubernetes workload

UnsafeKubernetes
spec:
  containers:
    - name: app
      image: example/app:latest
      securityContext:
        privileged: true

A floating image with full access to the node.

SaferKubernetes
spec:
  automountServiceAccountToken: false
  securityContext:
    runAsNonRoot: true
    seccompProfile: { type: RuntimeDefault }
  containers:
    - name: app
      image: registry.example.com/app@sha256:4d1f...
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities: { drop: ["ALL"] }
      resources:
        limits: { memory: 512Mi }

Meets the Kubernetes "restricted" Pod Security Standard. Enforce it per namespace so the unsafe version is refused.

Many of these patterns can be caught automatically before merge. A free SAST and code review tool such as Git Code Review reads code and history for them, and OpenSSF Scorecard checks repository and workflow settings.

The dependency row, handled

ForgeRepo™ puts a checked, recorded gate between every developer, every pipeline and the public registries. Free, MIT licensed, one container.