Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-10 16:19:16 -05:00
Commit graph

1,172 commits

Author SHA1 Message Date
Andrew Nesbitt
3146e80fae
Merge pull request #548 from simonchrz/fix/mirror-upstream-versions
List mirror versions from the configured upstream
2026-10-25 11:09:09 +00:00
Andrew Nesbitt
e141f16d17
Merge pull request #577 from simonchrz/fix/ssrf-forward-proxy
Check upstream destinations sent through a forward proxy
2026-10-25 11:09:02 +00:00
Andrew Nesbitt
0fc8ae71d1
Merge main into fix/mirror-upstream-versions
# Conflicts:
#	go.mod
#	go.sum
2026-10-25 10:49:49 +00:00
Andrew Nesbitt
847f4f351c
Merge main into fix/ssrf-forward-proxy
# Conflicts:
#	go.mod
#	go.sum
2026-10-25 10:36:38 +00:00
Andrew Nesbitt
c6ccbcb974
Merge pull request #588 from simonchrz/refactor/shared-enrichment
Use git-pkgs/enrichment for the enrichment API
2026-10-25 10:30:22 +00:00
Andrew Nesbitt
fcce59c71a
Merge pull request #589 from simonchrz/refactor/purl-normalise
Use purl's ComparisonForm for package identity
2026-10-25 10:30:17 +00:00
Andrew Nesbitt
42584c3845
Bump registries to v0.11.1 2026-10-25 10:29:20 +00:00
Andrew Nesbitt
910dadc220
Bump registries to v0.11.1 2026-10-25 10:17:37 +00:00
Andrew Nesbitt
1b641b6e90
Bump enrichment to v0.7.4 2026-10-25 10:15:58 +00:00
Andrew Nesbitt
2a4ce12248
Bump purl to v0.1.24 and drop redundant Swift lowercasing 2026-10-25 10:14:03 +00:00
Andrew Nesbitt
560a9918ff
Merge pull request #578 from simonchrz/fix/unambiguous-metadata-keys
Hash path-based metadata cache keys
2026-10-25 10:04:56 +00:00
Andrew Nesbitt
bd9f376f30
Merge pull request #580 from simonchrz/fix/git-request-limits
Bound Git requests by their body and response progress
2026-10-25 10:04:49 +00:00
Andrew Nesbitt
fcac1d5fae
Refuse '?' and '#' through rejectQueryChars in APK, Debian and RPM 2026-10-25 09:55:14 +00:00
Andrew Nesbitt
4d0d4a20ae
Spill large Git request bodies into the cache's temporary directory 2026-10-25 09:41:05 +00:00
Simon Chrzanowski
cc4a9c4046 Merge remote-tracking branch 'origin/main' into fix/mirror-upstream-versions
# Conflicts:
#	go.mod
#	go.sum
2026-10-24 19:18:21 +02:00
Simon Chrzanowski
c3d0448e6f Bump registries for streamed npm packuments
registries main streams npm packuments (git-pkgs/registries#109), so
listing the versions of packages whose packument is over 10 MiB, such as
typescript, no longer fails with "unexpected end of JSON input". It also
reads Maven versions from maven-metadata.xml (#114).

go.mod points at a pseudo-version of registries main until those are
released.
2026-10-24 19:03:38 +02:00
Simon Chrzanowski
0b2fa83590 Use registries' forward proxy check
registries' safehttp now checks the destination of requests sent through
a forward proxy (git-pkgs/registries#116), so the proxy's own guard is
removed and newUpstreamClient is back to its earlier form.

go.mod points at a pseudo-version of registries main until #116 is
released.
2026-10-24 19:00:57 +02:00
Simon Chrzanowski
de308e48e0 Use purl's ComparisonForm for denylist and NuGet retention keys
The per-ecosystem rules for when two spellings name the same package
(PEP 503, NuGet case, Composer's leading v, conda channel, OCI paths,
Swift registry case) now live in purl. The denylist keeps only the
proxy's own pkg:generic/swift-registry spelling and the gem platform
match. Keys, cache paths and stored PURLs are unchanged.
2026-10-24 18:58:23 +02:00
Simon Chrzanowski
fc299593ac Answer 404 for packages the registry doesn't have 2026-10-24 18:56:19 +02:00
Simon Chrzanowski
94668c917b Use git-pkgs/enrichment for the enrichment API
internal/enrichment now adapts the shared module instead of calling
registries and OSV itself. License categories in the API and the web UI
come from enrichment.CategorizeLicense, which normalizes the license
first. Vulnerability fixed versions come from the affected range of the
queried package.
2026-10-24 15:55:37 +02:00
simon chrzanowski
6a988050f6
Merge pull request #587 from git-pkgs/fix-docker-mirror-inputs
Preserve Docker registries and SBOM versions when mirroring
2026-10-24 15:08:26 +02:00
Andrew Nesbitt
a29f9fc234
Preserve Docker registries and SBOM versions when mirroring 2026-10-24 13:06:12 +01:00
Simon Chrzanowski
fd4f067a1b Keep legacy metadata whose blob matches its digest
Ignoring every row still at its unhashed path dropped the whole metadata
cache on upgrade, including on Linux and S3 where keys never collided,
and left nothing to serve while the upstream was down.

A legacy row's blob is now hashed once and compared with the row's
content_digest. When it matches, the blob is this key's document: it is
copied to the hashed path and the row moved there, so it is served
fresh or as a stale fallback as before. Only a mismatch, where another
key's document overwrote the shared file, or a row without a digest is
dropped and fetched again.

The legacy blobs are queued for deletion once no key differing only in
case still reads its legacy path, which on a case-insensitive
filesystem is the same file.
2026-10-24 13:10:40 +02:00
Simon Chrzanowski
d64bfc6d36 Refuse '?' and '#' in paths in more handlers
Debian, RPM, APK, PyPI, CRAN and Maven build upstream URLs from the
decoded request path, where a '?' or '#' would end the path and cache
one file under another's name. Only the server middleware refused them;
Arch and FreeBSD also check in the handler. Check in these handlers
too, so they stay safe when mounted without the middleware.
2026-10-24 12:57:20 +02:00
Simon Chrzanowski
aa0460a15b Bound Hex and gem cooldown reads by the metadata size limit
With cooldown on, the Hex handler read the /packages response, its decompressed payload and the Hex API's JSON without a limit, and the gem handler decoded the versions API's JSON the same way. A misbehaving upstream could make the proxy hold any amount in memory, and a small gzip payload could expand without bound.

These reads now go through ReadMetadata, so metadata_max_size applies to each, and to the Hex payload after decompression as the NuGet handler does. A response over the limit fails the request with 502.
2026-10-24 12:55:24 +02:00
Simon Chrzanowski
09e41d0272 Fail closed in gem, Hex and PyPI cooldown
RubyGems, Hex and PyPI keep publish times outside the metadata clients
read, so cooldown fetches them from a second API. When that fetch
failed, was rate limited, or returned something that didn't parse, the
compact index, Hex package or simple page was served unfiltered, and a
Hex package that couldn't be re-encoded was passed on as upstream sent
it. A flaky or rate-limited timestamp API let every version in
cooldown through.

These now fail with 502, or with 503 and Retry-After when the rate
limit gate held the request, as the Go handler already does. PyPI file
downloads check the same way. A PyPI package the JSON API answers 404
for still has no times and is served unfiltered, as before.

npm, Composer, pub.dev and PyPI JSON metadata that couldn't be
rewritten was also passed on unfiltered unless the package had denied
versions. It now fails with 502 whenever cooldown applies too. npm
tarball downloads whose packument couldn't be fetched skipped the
cooldown check and downloaded straight from upstream; they now fail.

All of this only applies when cooldown covers the package: packages
exempted with a duration of 0 skip the timestamp lookups, and the gem
and Hex handlers serve them through the metadata cache as when cooldown
is off.
2026-10-24 12:55:24 +02:00
Simon Chrzanowski
77bd66acab Bound Git requests by their body and response progress
upload-pack was started with its stdin attached to the request body, after
the read deadline had been extended to two hours, and ls-refs requests took
no slot. A client that sent part of a request and then nothing kept an
upload-pack process waiting, and any number of them could.

Stateless requests are complete once sent, so the handler now reads the
body in full first, within the server's read timeout, keeping bodies past
1 MiB in a temporary file. Only then are the deadlines extended for the
response. info/refs and ls-refs share their own slots, four per CPU, so they
still never wait behind clones. Each response write is bounded to two
minutes, so a client that stops reading a pack frees its slot instead of
holding it until the request timeout.
2026-10-24 12:55:14 +02:00
Simon Chrzanowski
d2c6587996 Hash path-based metadata cache keys
Conan, CRAN, Conda, RPM, the main Debian archive and the default Maven
repository keyed cached metadata by the request path with '/' replaced
by '_'. Names and versions may contain '_', so foo/bar_1.0 and
foo_bar/1.0 shared an entry, and with cache_metadata one reference's
recipe files were served for the other. Conan also appended the query
with '_', so a path without a query could collide with a shorter path
plus one.

Hash the upstream URL, the path and, for Conan, the query instead, as
the APK, Arch, generic and Helm handlers already do. Each part is
length-prefixed so no two inputs share a key.
2026-10-24 12:55:00 +02:00
Andrew Nesbitt
562ffb3ecd
Merge pull request #586 from simonchrz/fix/gem-filename-length
Refuse overlong gem filenames
2026-10-24 11:44:41 +01:00
Andrew Nesbitt
319253a1ce
Merge pull request #584 from simonchrz/fix/gem-index-gzip
Filter gzipped gem compact indexes under cooldown
2026-10-24 11:44:26 +01:00
Andrew Nesbitt
1c3f1aa255
Merge pull request #583 from simonchrz/fix/verify-all-digests
Verify every OCI digest and Helm charts before caching
2026-10-24 11:44:04 +01:00
Andrew Nesbitt
cefdbe6b70
Merge pull request #582 from simonchrz/fix/npm-tarball-unverified
Don't fetch npm tarballs whose packument can't be read
2026-10-24 11:43:41 +01:00
Andrew Nesbitt
5d9df6047c
Merge pull request #581 from simonchrz/fix/generic-auth-paths
Require generic_paths for generic upstreams with credentials
2026-10-24 11:43:11 +01:00
Andrew Nesbitt
9ef99e2ebc
Merge pull request #576 from simonchrz/fix/registry-token-scope
Send registry tokens only to the token service's own registry
2026-10-24 11:38:40 +01:00
Andrew Nesbitt
08e9e7ddea
Merge pull request #574 from simonchrz/fix/mirror-csrf-ui-framing
Refuse cross-site mirror jobs and framing of the UI
2026-10-24 11:35:18 +01:00
Andrew Nesbitt
4a1e4fe4e4
Merge pull request #573 from simonchrz/feat/maven-cooldown
Apply cooldown to Maven
2026-10-24 11:34:46 +01:00
Andrew Nesbitt
b855c5b744
Merge pull request #572 from simonchrz/fix/flaky-tests
Fix flaky tests on Windows and slow runners
2026-10-24 11:33:34 +01:00
Andrew Nesbitt
7aee2bb078
Merge pull request #571 from git-pkgs/docs/oci-pull-only
Document that the OCI handler is pull-only
2026-10-24 11:33:00 +01:00
Andrew Nesbitt
44b81a2f1e
Merge pull request #570 from simonchrz/feat/fetch-leases
Fetch each cache miss once across instances
2026-10-24 11:32:37 +01:00
Andrew Nesbitt
81fbbe6c28
Merge pull request #569 from simonchrz/fix/multi-replica-locks
Coordinate instances sharing a Postgres database
2026-10-24 11:31:59 +01:00
Andrew Nesbitt
4bc42c4fb1
Merge pull request #561 from simonchrz/feat/conda-shards
Serve conda sharded repodata through cooldown and the cache
2026-10-24 11:31:31 +01:00
Simon Chrzanowski
ce39583784 Verify Helm charts before they are cached
Without artifact streaming, a chart was fetched with no expected
digest, committed to the cache, and only then compared with the
index's digest by serveChart, which cleared it again. Until then the
unverified chart was a cache entry like any other.

The index digest is now always passed to the fetch, so a mismatching
chart is discarded before it is committed, as it already was when
streaming. serveChart's check stays for charts that were cached before.
2026-10-24 08:12:59 +02:00
Simon Chrzanowski
ff10ab3491 Verify sha512 OCI digests and refuse ones that can't be checked
Manifests and digest-addressed artifacts were only checked against
sha256 digests. A sha512 digest, which the OCI spec also allows, or a
digest with any other algorithm was fetched and cached unchecked: a
manifest requested as sha512:<hex>, or a tag whose Docker-Content-Digest
header named one, was stored under that digest whatever the body was.

Manifests are now checked against any digest in the reference or the
Docker-Content-Digest header with the digest's own algorithm, and one
the proxy can't compute is answered with DIGEST_INVALID and not cached.
GetOrFetchArtifactFromURLWithDigest verifies sha512 (and sha384)
digests through the declared-integrity check, so a mismatch is never
committed, and refuses other algorithms with ErrUnsupportedDigest
instead of fetching unverified. Blob routes still only accept sha256.
2026-10-24 08:12:59 +02:00
Simon Chrzanowski
13cb62c236 Store metadata under a hash of its cache key
Cached metadata was stored at _metadata/<ecosystem>/<key>/metadata. Keys
are case-sensitive, but on a case-insensitive filesystem such as APFS or
NTFS, file:// storage put keys that differ only in case in one file:
github.com/Sirupsen/logrus and github.com/sirupsen/logrus each had a
row, and whichever was cached last was served for both. Keys may also
hold names Windows reserves or end in a dot.

The path now carries a SHA-256 of the exact key. Keys that already are
SHA-256 hex digests, as most hashed keys are, keep their path.

A row still pointing at an unhashed path may hold another key's bytes,
so it is neither served nor revalidated; the document is fetched again
and stored at the new path, and the old blob is queued for deletion.
2026-10-24 08:09:25 +02:00
Simon Chrzanowski
9500c5b5a4 Check upstream destinations sent through a forward proxy
The upstream client refuses loopback, private and link-local addresses
when it dials, but with HTTP_PROXY or HTTPS_PROXY set it dials the
forward proxy, so only the forward proxy's address was checked. A URL
taken from upstream metadata, such as a Helm chart URL, a Composer dist
URL, a token realm or a redirect, could then reach 169.254.169.254 or
an internal host through the forward proxy.

The transport's proxy function now resolves the destination of every
request it sends to a forward proxy and applies the same rules,
including upstream.allow_loopback and upstream.allow_private_hosts,
before handing it over. A name that cannot be resolved locally is still
passed on, since some networks only resolve public names at the
forward proxy. Direct requests are unchanged.

Git fetches and token requests use the same transport, so they are
covered too.
2026-10-24 08:07:13 +02:00
Simon Chrzanowski
b6d7575b88 Don't fetch npm tarballs whose packument can't be read
An uncached tarball is checked against the integrity its packument
declares. When the packument couldn't be fetched or was refused, for
example because it is larger than metadata_max_size or the registry
answered 503, the download went ahead with no integrity at all, and
the unverified tarball was cached and served to every later client.

Without the packument the download now fails like the Composer one
does: 502, or 404 when the registry says the package doesn't exist.
Nothing is fetched or cached. Tarballs already in the cache are still
served, since they were verified when they were stored, and a packument
that lists the version without a usable checksum still falls back to
an unverified fetch as before.
2026-10-24 08:06:10 +02:00
Simon Chrzanowski
2ea8915e06 Filter gzipped gem compact indexes under cooldown
With cooldown on, /info/{name} is fetched from upstream and versions still in cooldown are removed line by line. The fetch passed on the client's Accept-Encoding, and setting that header turns off the transport's transparent decompression, so for any client that accepts gzip, Bundler included, the filter scanned gzip bytes, matched no versions and sent the compressed index on with upstream's Content-Encoding. Cooldown filtered nothing for those clients.

The fetch now passes on only Accept, so the transport decompresses the index before it is filtered. If-None-Match and If-Modified-Since are no longer passed on either: the client's validators came from a filtered copy, not from upstream's file.

The filtered index no longer carries upstream's ETag, Last-Modified, Digest, Repr-Digest or range headers, which describe the unfiltered file; Bundler checks the digest against the body it gets.

The index is now read through ReadMetadata before anything is written. bufio.Scanner stopped at a line over 64 KB and the handler sent what it had so far as a complete 200; a read error or an index over the metadata size limit now answers 502.
2026-10-24 08:03:16 +02:00
Simon Chrzanowski
112e9c89ec Require generic_paths for generic upstreams with credentials
A generic upstream forwards any path below /generic/{name}/. The
documented mise setup puts a GITHUB_TOKEN on api.github.com to raise
the rate limit, so anyone who could reach the proxy could make any GET
request with that token, including for private repositories it can
read, and the answers went into the shared metadata cache. upstream.git
already refuses an upstream with credentials unless it lists its repos.

upstream.generic_paths now lists, per generic upstream, the paths it
may serve, with the same pattern syntax as git repos. Other paths
answer 404 without reaching the upstream, and so does a path holding
'%', '?', '#' or a backslash, which the upstream could read as a
different path than the one that matched. Config validation refuses a
generic upstream that upstream.auth has credentials for unless it has
generic_paths, which also covers a github.com token reaching the
generic github upstream. Upstreams without credentials keep serving
every path.

The docs and example config show the paths mise needs and recommend a
fine-grained token limited to public repositories.
2026-10-24 08:02:22 +02:00
Simon Chrzanowski
aa3adbfa63 Bound the registry token and challenge caches
The transport remembered a challenge for every /v2/<repository> it was
ever challenged on and never forgot one, so a long-running proxy, or a
metadata document naming many registries, grew the map without limit.
Tokens were only pruned of expired entries, and the expiry came from
the token service.

Challenges are now kept per registry host, holding only the realm and
service, and the pull scope for a request is built from its path, so
pulling many repositories from one registry keeps one entry. Both
caches have a size limit, evicting an arbitrary challenge or the token
that expires soonest, and a token is reused for at most an hour,
whatever lifetime the token service claims.
2026-10-24 08:02:10 +02:00
Simon Chrzanowski
351fe7c0b5 Send registry tokens only to the token service's own registry
After a 401 the transport fetches a token from the challenge's realm,
using credentials configured for that realm, and sends it to the host
that issued the challenge. It trusted that host if it had any configured
credentials, even unrelated ones, or shared the realm's registrable
domain. The second rule makes every tenant on a shared domain such as
jfrog.io, azurecr.io or pkg.dev the same site, and the first hands a
token minted from one registry's credentials to another registry the
proxy happens to have a key for.

A credentialed token now goes only to a registry on the realm's own
host and port, to one configured with the same credentials as the
realm, or to Docker Hub and GitLab.com, whose registries use a token
service on another host. Anonymous tokens are unaffected.

The scope in the token request also came from the challenge, so a
registry could ask for push access or for other repositories. The proxy
only reads, so it now always asks for pull access to the repository in
the request path.
2026-10-24 08:00:48 +02:00