Skip to content

fix: decode PURL percent-encoding in versions and package paths - #244

Open
wickedOne wants to merge 1 commit into
git-pkgs:mainfrom
wickedOne:purl-version-encoding
Open

fix: decode PURL percent-encoding in versions and package paths#244
wickedOne wants to merge 1 commit into
git-pkgs:mainfrom
wickedOne:purl-version-encoding

Conversation

@wickedOne

Copy link
Copy Markdown
Contributor

another claude generated patch to fix the following behaviour:
image

Problem

Package versions containing + were unusable in the UI. Reported against nmap_7.91+dfsg1+really7.80+dfsg1-2ubuntu0.1_amd64.deb from the Ubuntu archive (PROXY_UPSTREAM_DEBIAN=http://archive.ubuntu.com/ubuntu), which displayed as @7.91%2Bdfsg1%2Breally7.80%2Bdfsg1-2ubuntu0.1.

The artifact download itself was always fine — the bug was in reading the version back out of the PURL. PURL percent-encodes + in path components, so the version is stored as pkg:deb/nmap@7.91%2Bdfsg1%2B…. Two places treated the raw substring after @ as the version:

  1. DisplayVersion.Version() (strings.LastIndex(purl, "@")) and the SQL SUBSTR(v.purl, INSTR(v.purl,'@') + 1) in GetRecentlyCachedPackages never decoded, so %2B leaked into the dashboard and package pages.
  2. Round-trip — that encoded string went into the version link. chi routes on r.URL.RawPath whenever the URL contains an escape, so the handler received 7.91%2B… back and re-encoded it into pkg:deb/nmap@7.91%252B…. The version page 404'd.

This affects any ecosystem with non-alphanumeric version characters, not just Debian: npm scoped packages had the same problem via %40.

Changes

  • internal/database/types.go — add VersionFromPURL (strips qualifiers/subpath, then percent-decodes); Version.Version() uses it. Add Version.DisplayPURL() for a readable PURL in the UI, decoding all path components.
  • internal/database/queries.goGetRecentlyCachedPackages selects v.purl and derives the version in Go. This also removes the duplicated Postgres-specific SUBSTRING/POSITION variant of the query.
  • internal/server/resolve.gosplitWildcardPath percent-decodes each segment after splitting, so an encoded %2F inside a name is not mistaken for a separator.
  • Templates use DisplayPURL for version headings and labels.

Storage is unchanged. Stored PURLs keep their canonical encoding (%2B, %40), which is what the API, cache keys and all lookups use. This is display and inbound routing only, so no cache migration is needed. Old encoded URLs still resolve, so existing bookmarks keep working.

Security

validatePackagePath now validates the decoded segments and rejects .. path elements. This closes a traversal vector: package names are rejoined and interpolated straight into upstream URLs by several registries (e.g.
fmt.Sprintf("%s/web/packages/%s/DESCRIPTION", baseURL, name) for CRAN), and Go sends dot-segments verbatim for the upstream to resolve. GET /api/package/cran/pkg%2F..%2F..%2Fadmin now returns 400 instead of reaching upstream as /web/packages/pkg/../../admin/DESCRIPTION.

Note this is stricter than before the change: the original code had no .. check at all, so a literal ../.. also passed validation.

Testing

  • New: internal/database/version_purl_test.go (VersionFromPURL, DisplayPURL, and GetRecentlyCachedPackages decoding under both sqlite and Postgres).
  • Extended: resolve_test.go (segment decoding, traversal rejection), server_test.go (version page reachable via both encoded and decoded URLs), debian_test.go (pool paths with + and ~).
  • Each new test was confirmed to fail without the fix.
  • Verified end-to-end against the real Ubuntu archive with 12 packages across deb/npm/cargo/pypi, following every version link the UI generates: all 200, no encoding leaks. Version shapes covered include multiple +, + after -, ~dfsg, leading 0~, and npm @scope. ~ is correctly left unencoded (it is PURL-safe).
  • Full suite and golangci-lint pass; the 6 remaining staticcheck warnings are pre-existing in untouched test files.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant