Repository navigation
Hosted Composer rewrite keeps the entry's transport-options, so Composer sends a private repository's auth headers to the hosted patch URL #399
Description
[agent] Found by the scheduled Composer bug-hunt routine (ledger #321).
Summary
Composer copies a repository's options (for example "options": {"http": {"header": ["X-Private-Token: …"]}}, a common way to authenticate a private Composer repository or Satis) into every package it resolves from that repository, as the lock entry's transport-options. scan --mode hosted sets the entry's dist.url to the hosted patch archive and removes source and dist.mirrors, but it keeps transport-options. Composer applies a package's transport-options to that package's dist download. So the next composer install sends the private repository's credential header to the hosted patch host.
Impact
- A private registry credential is disclosed to a third-party origin (the hosted patch server) on every install from the rewritten lock. That includes CI and fresh clones, and nothing warns about it.
- The same applies to any other stream option the repository set, such as
ssl.local_cert,ssl.verify_peer: false, orhttp.proxy. Those were meant for the private repository only and now apply to the hosted URL. - Vendored mode isn't affected:
rewrite_lock_entryreplacestransport-optionswith{"symlink": false}.
Repro
Everything runs locally: a two-file Composer repository on 127.0.0.1:8765, plus a small mock of the patch API on 127.0.0.1:8766 (it serves patches/batch, patches/package → a granted artifacts[].integrity.sha1, by-package, view and the zip). This is the same shape e2e_redirect_composer_build mocks.
# private repo with a header option (served by python -m http.server 8765)
cat > composer.json <<'J'
{"name":"t/app",
"repositories":[{"type":"composer","url":"http://127.0.0.1:8765",
"options":{"http":{"header":["X-Private-Token: s3cret"]}}},
{"packagist.org":false}],
"require":{"acme/rlib":"1.0.0"},"config":{"secure-http":false}}
J
composer update -q
jq '.packages[0]["transport-options"]' composer.lock # {"http":{"header":["X-Private-Token: s3cret"]}}
socket-patch scan --mode hosted --json --yes --api-url http://127.0.0.1:8766 --org org --api-token fake
jq '.packages[0] | {dist, "transport-options"}' composer.lock
# dist.url -> http://127.0.0.1:8766/patch/composer/acme/rlib/1.0.0/tok/<uuid>/rlib-1.0.0.zip
# transport-options -> {"http":{"header":["X-Private-Token: s3cret"]}} <- kept
rm -rf vendor && COMPOSER_HOME=$(mktemp -d) composer install -q
# mock hosted server log:
# GET /patch/composer/acme/rlib/1.0.0/tok/<uuid>/rlib-1.0.0.zip headers={..., 'X-Private-Token': 's3cret', ...}The install succeeds and the patched bytes land, so there's no visible error. The header is only visible on the hosted server's side.
Expected vs actual
- Expected:
docs/testing/composer-compatibility.md("What each mode writes") says the hosted rewrite retargets the dist to the hosted archive and removes the entry'ssourceanddist.mirrors, because they belong to the original origin. The entry'stransport-optionsalso belong only to the original repository, so the hosted rewrite should drop them too. It could warn, likeredirect_composer_dist_mirrors_removeddoes, and the fragment revert would restore them. The alternative is to refuse. Credentials for the private registry must never be sent to the patch host. - Actual:
transport-optionsstays on the entry verbatim, and Composer sends the header to the hosted URL.redirected: 1with no warning.
Matrix (Linux, main f6b7fb9 = 4.0.0)
| Composer (PHP 8.3) | Lock carries transport-options from repo options |
Header sent to hosted URL on composer install |
|---|---|---|
| 1.10.28 | yes | yes |
| 2.2.30 | yes | yes |
| 2.8.12 | yes | yes (twice) |
This is OS-independent: the lock text and Composer's own download options drive it. I didn't bisect: every release that carries the current hosted composer rewriter behaves this way on 4.0.0.
Suspect code
crates/socket-patch-core/src/patch/redirect/composer_source.rs:292(apply_dist_edit). It dropssource(plan_source_drop, line 241) anddist.mirrors, but never looks at the entry'stransport-options.crates/socket-patch-core/src/patch/redirect/mod.rs:4953(rewrite_composer_lock).
Backlog review — 2026-10-08
Priority: P2 → P1. Forwarding private repository authorization headers to the hosted patch URL is credential disclosure. Open PR #1026 should receive security-focused review.
Activity
mikolalysenko commentedon Oct 1, 2026
[agent] Triage: priority:p2 (Composer). Confirmed on main f6b7fb9: the hosted Composer rewrite never touches
transport-options(the only writer of that key is the vendored path incrates/socket-patch-core/src/vendor/composer_lock.rs, which supersedes it with{"symlink": false}). Not a duplicate, and not covered by open PR #358 (that PR is about nestedextra.distlookup). Treating as a correctness/security bug (credential sent to a third-party origin).Generated by Claude Code
mikolalysenko commentedon Oct 1, 2026
[agent] Re-triage on main
2463257(v5 consolidation, #277): still reproduces. The v5 hosted rewrite (no ledger) still leaves the entry'stransport-optionsin place after it pointsdist.urlat the hosted origin.Lock entry before:
dist.url=https://repo.example.com/dist/acme-tool-1.0.0.zip,"transport-options": {"http": {"header": ["X-Private-Token: s3cret"]}}. Aftersocket-patch scan --mode hosted --ecosystems composer(mock patch API,grantedwith sha1):{"name": "acme/tool", "version": "1.0.0", "dist": {"type": "zip", "url": "https://patch.socket.dev/patch/composer/acme/tool/1.0.0/tok/<uuid>/acme-tool-1.0.0.zip", "reference": "deadbeef", "shasum": "0123…"}, "type": "library", "transport-options": {"http": {"header": ["X-Private-Token: s3cret"]}}}So Composer will still send the private repository's header to
patch.socket.dev. Real-Composer behaviour for that lock shape is as shown in the original report.Generated by Claude Code
mikolalysenko commentedon Oct 1, 2026
[agent] Composer bug-hunt routine (ledger #321): still reproduces on main
61cfb9b, after #358 changed the Composer rewriter. I also found a second impact of the same kepttransport-options. It needs no private repository: on Composer 1, the next install of a hosted-redirected path-repository entry fails outright.Path-repo variant. For a relative
"type": "path"repository, Composer writes"transport-options": {"relative": true}(plus"symlink": …if the repo sets it) on the lock entry. The hosted rewrite turns the entry intodist.type: zipwith the hosted url and shasum, but keeps thosetransport-options. Composer 1 passes a package'stransport-optionsintostream_context_create()for the dist download. These keys aren't stream-wrapper options, so the download dies:Removing only
transport-optionsfrom the rewritten entry makes the same install succeed with the patched bytes.transport-optionsstrippedsocket-patch scanon61cfb9b(×3) and on v4.0.0Header leak (the original report). Re-checked on
61cfb9bwith atype: composerrepository carryingoptions.http.header. The rewritten entry still hastransport-options: {"http":{"header":["X-Private-Token: s3cret"]}}.Dropping (or refusing)
transport-optionsin the hosted rewrite, as proposed above, fixes both.relativeandsymlinkonly mean anything for apathdist.Generated by Claude Code
mikolalysenko commentedon Oct 9, 2026
v5 release blocker (P1). An ordinary hosted Composer rewrite forwards existing private-repository transport credentials to the patch download. Fix the source transition before release; PR #1026 is pending.
This follows the maintainer's release scope: one normally completing CLI instance, prioritizing valid-lockfile patch/install behavior, compatibility, and actionable CLI UX.