Conversation
pnpr persisted and surfaced each bearer token's `readonly` and `cidr_whitelist` restrictions but never applied them during authorization, so a token marked read-only or pinned to a network could still publish and mutate packages from anywhere (GHSA-rp44-v426-6m56 / CAND-PNPM-034). The token lookup on the auth hot path resolved a raw token to just its username and dropped the restriction fields. This adds `TokenBackend::lookup_record`, a default trait method that resolves the full `TokenRecord` (username plus restrictions) by reusing each backend's existing `find_by_key`, so the local SQLite store and the libsql/sqlx backends all load `readonly` and `cidr_whitelist` with no per-backend change. Enforcement runs in an axum middleware layer ahead of every route handler, so a restricted token is rejected before a write handler buffers its (up to 100 MiB) request body: - Read-only tokens are confined to non-mutating methods. Every write surface (publish, unpublish, dist-tag, adduser, logout, token revoke) is a PUT or DELETE and returns 403; GET/HEAD/OPTIONS and the resolver POSTs pass. - CIDR-pinned tokens are checked against the real socket peer address, captured via a `PeerAddr` `ConnectInfo` newtype wired through `axum::serve` (the blanket impl axum ships covers only the bare `TcpListener`, not our `NodelayTcpListener` wrapper). Client-supplied forwarding headers are never trusted. CIDR matching is done in-house (no new dependency) and handles IPv4/IPv6, /0 through /32 and /128, bare-host exact match, and IPv4-mapped IPv6 peers. A malformed entry or an unavailable peer fails closed. Basic-auth, anonymous, and unknown or revoked tokens pass through unchanged and remain subject to the per-package access policy. A backing-store failure during lookup surfaces as a 5xx rather than silently skipping the check. This is pnpr-only and does not change the pnpm CLI or pacquet. Written by an agent (Claude Code, claude-opus-4-8).
Reorder the PeerAddr derive list to the workspace's prefix_then_alphabetical order, move the restricted-token test seam out of the auth source into a small in-test TokenBackend mock (the perfectionist lint keeps test code in external modules), and return an error instead of unimplemented! in that mock's unused issue path. Written by an agent (Claude Code, claude-opus-4-8).
PR Summary by Qodofix(pnpr): enforce bearer-token readonly and CIDR restrictions Description
Diagram
High-Level Assessment
Files changed (4)
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds a ChangesBearer Token Self-Restrictions Enforcement
Sequence Diagram(s)sequenceDiagram
participant Client
participant Listener as TCP Listener
participant Middleware as authenticate middleware
participant TokenBackend
participant Handler as Route Handler
participant Client2
Listener->>Middleware: accept peer, inject ConnectInfo<PeerAddr>
Client->>Middleware: HTTP request + Authorization: Bearer <token>
Middleware->>TokenBackend: lookup_record(raw_token)
alt store error
TokenBackend-->>Middleware: Err
Middleware-->>Client: 500 Internal Server Error
else token not found
TokenBackend-->>Middleware: Ok(None)
Middleware->>Handler: forward (anonymous identity)
Handler-->>Client: response
else token found with TokenRecord
TokenBackend-->>Middleware: Ok(Some(TokenRecord))
alt readonly=true AND is_write_method(http_method)
Middleware-->>Client: 403 Forbidden
else cidr_whitelist non-empty AND peer IP outside range
Middleware-->>Client: 403 Forbidden
else all checks pass
Middleware->>Handler: forward with Identity in extensions
Handler-->>Client2: response
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~65 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Code Review by Qodo
1. Identity cloned from extensions
|
|
Code review by qodo was updated up to the latest commit ccecc85 |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #12574 +/- ##
==========================================
+ Coverage 87.99% 88.05% +0.06%
==========================================
Files 324 326 +2
Lines 46008 46415 +407
==========================================
+ Hits 40483 40870 +387
- Misses 5525 5545 +20 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Authentication now happens once, in the request middleware: it resolves the Authorization header to an Identity (looking a bearer token up as a full record so its read-only / CIDR restrictions are enforced there), and stashes that Identity in request extensions. Handlers read it back through an AuthedCaller extractor instead of each re-inspecting the header. This removes the second token lookup that authed requests previously paid (middleware gate plus the handler's own resolution) — a real round-trip for the remote libsql backend — and the policy/identity race that two independent lookups allowed. The per-handler enforce_access / resolve_identity / caller_username helpers collapse into the existing synchronous authorize() and a pure require_caller() check against the already-resolved identity. Behavior is unchanged: the same 401/403/5xx outcomes, the same per-package policy, and the same read-only / CIDR enforcement, now driven from one place. Written by an agent (Claude Code, claude-opus-4-8).
|
Code review by qodo was updated up to the latest commit d7b6ecf |
1 similar comment
|
Code review by qodo was updated up to the latest commit d7b6ecf |
Integrated-Benchmark Report (Linux)Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD. Scenario: Isolated linker: fresh restore, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.399437346079999,
"stddev": 0.18161216626705762,
"median": 4.32663907088,
"user": 3.6947663200000003,
"system": 3.3873963599999994,
"min": 4.2690582378799995,
"max": 4.87252955288,
"times": [
4.87252955288,
4.322318999879999,
4.36115765688,
4.466671904879999,
4.481496369879999,
4.2690582378799995,
4.32401237588,
4.27806408788,
4.28979850888,
4.32926576588
]
},
{
"command": "pacquet@main",
"mean": 4.45503097498,
"stddev": 0.1498273889623892,
"median": 4.392776510879999,
"user": 3.75682172,
"system": 3.433847759999999,
"min": 4.32767041188,
"max": 4.81604443188,
"times": [
4.81604443188,
4.53180308388,
4.38086097388,
4.44820673988,
4.3582860308799996,
4.404692047879999,
4.34337149688,
4.37158010888,
4.56779442388,
4.32767041188
]
},
{
"command": "pnpr@HEAD",
"mean": 2.9328507220800004,
"stddev": 0.14652670598717019,
"median": 2.90435282138,
"user": 2.72554682,
"system": 3.0131685599999996,
"min": 2.77569273688,
"max": 3.19748778088,
"times": [
3.19748778088,
2.88076446688,
2.83126539588,
2.92794117588,
2.79460937388,
3.15014716588,
2.95963784688,
2.99740018788,
2.81356108988,
2.77569273688
]
},
{
"command": "pnpr@main",
"mean": 2.91269451668,
"stddev": 0.13422940927482258,
"median": 2.8547182488800003,
"user": 2.76234572,
"system": 3.0078916599999994,
"min": 2.77846751188,
"max": 3.18819960388,
"times": [
2.82385403288,
2.86511005788,
3.08706894988,
2.84432643988,
3.18819960388,
2.9081502338800003,
2.8255144478800003,
2.9902940998800003,
2.77846751188,
2.8159597888800003
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.6459712147200001,
"stddev": 0.009293329523401821,
"median": 0.6482859358200002,
"user": 0.39606252,
"system": 1.3212601,
"min": 0.6280953138200001,
"max": 0.65664798982,
"times": [
0.6518051998200001,
0.6513074108200001,
0.6419648248200001,
0.6556161018200001,
0.6457579198200001,
0.6339888508200001,
0.64371458382,
0.6280953138200001,
0.6508139518200001,
0.65664798982
]
},
{
"command": "pacquet@main",
"mean": 0.64690693702,
"stddev": 0.011511484939610772,
"median": 0.6484007538200001,
"user": 0.39706012,
"system": 1.3219697999999998,
"min": 0.6278555628200001,
"max": 0.6637676798200001,
"times": [
0.6525698628200001,
0.6375752238200001,
0.6278555628200001,
0.6496238688200001,
0.6637676798200001,
0.6349749478200001,
0.6625467888200001,
0.6504548548200001,
0.6471776388200001,
0.64252294182
]
},
{
"command": "pnpr@HEAD",
"mean": 0.71525289452,
"stddev": 0.0958817421689313,
"median": 0.68925146382,
"user": 0.41202201999999993,
"system": 1.3622793999999998,
"min": 0.6679028548200001,
"max": 0.9851431058200001,
"times": [
0.70511895682,
0.6679028548200001,
0.6914089938200001,
0.6947806498200001,
0.67668259082,
0.6870939338200001,
0.7054096568200001,
0.6686385938200001,
0.67034960882,
0.9851431058200001
]
},
{
"command": "pnpr@main",
"mean": 0.7263077014200001,
"stddev": 0.06472210069978467,
"median": 0.7048637668200001,
"user": 0.41079901999999996,
"system": 1.3650557999999997,
"min": 0.68773153082,
"max": 0.9072144248200001,
"times": [
0.7174327058200001,
0.7122723298200001,
0.6980663588200001,
0.69958475482,
0.68773153082,
0.7084128768200001,
0.6993948948200001,
0.9072144248200001,
0.7316524808200001,
0.70131465682
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.784806553939999,
"stddev": 0.051692890027121005,
"median": 4.78401565964,
"user": 3.9123415199999996,
"system": 3.41246942,
"min": 4.69533308564,
"max": 4.880458217639999,
"times": [
4.7490338336399995,
4.73942480564,
4.815439555639999,
4.76733454764,
4.78122701764,
4.69533308564,
4.80455914364,
4.78680430164,
4.82845103064,
4.880458217639999
]
},
{
"command": "pacquet@main",
"mean": 4.702901542739999,
"stddev": 0.06595937565760361,
"median": 4.68297182164,
"user": 3.8843282199999996,
"system": 3.35995182,
"min": 4.6262130376399995,
"max": 4.826499612639999,
"times": [
4.73991311364,
4.68163800464,
4.78369041664,
4.633630935639999,
4.73079787064,
4.6262130376399995,
4.826499612639999,
4.67218001664,
4.65014678064,
4.68430563864
]
},
{
"command": "pnpr@HEAD",
"mean": 2.8203174957400003,
"stddev": 0.12530473719722843,
"median": 2.76862161414,
"user": 2.59509322,
"system": 2.9365975200000003,
"min": 2.73953666764,
"max": 3.15275536764,
"times": [
2.7666446596400003,
2.7631507756400002,
2.77059856864,
3.15275536764,
2.73953666764,
2.79085246564,
2.84875769764,
2.74545613664,
2.87695020064,
2.74847241764
]
},
{
"command": "pnpr@main",
"mean": 2.90633005734,
"stddev": 0.15254060861757407,
"median": 2.9007390866400002,
"user": 2.6057043199999996,
"system": 2.9229999199999996,
"min": 2.70839745764,
"max": 3.15783312664,
"times": [
2.7579022176400003,
2.7441221176400004,
2.97251110964,
3.15783312664,
3.06888513564,
2.86560446564,
2.9358737076400003,
2.70839745764,
2.8153419096400003,
3.03682932564
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.3768552383400001,
"stddev": 0.015458850902761808,
"median": 1.37408230384,
"user": 1.41905026,
"system": 1.71776352,
"min": 1.3489553138400001,
"max": 1.40517285584,
"times": [
1.40517285584,
1.3902190568400001,
1.37043762184,
1.36637611684,
1.37470144884,
1.39122850284,
1.3489553138400001,
1.37496138484,
1.3730369228400001,
1.3734631588400001
]
},
{
"command": "pacquet@main",
"mean": 1.45101749064,
"stddev": 0.06753995673148579,
"median": 1.4369207243400002,
"user": 1.4281841599999998,
"system": 1.7814400200000002,
"min": 1.40661960284,
"max": 1.6382063068400001,
"times": [
1.42621018884,
1.41379244284,
1.6382063068400001,
1.44761934384,
1.45006461684,
1.44169645784,
1.40661960284,
1.4410097638400001,
1.41212449784,
1.43283168484
]
},
{
"command": "pnpr@HEAD",
"mean": 0.70904035164,
"stddev": 0.026980709483193643,
"median": 0.7062568768400002,
"user": 0.3907085599999999,
"system": 1.32451872,
"min": 0.67346204084,
"max": 0.7551644358400001,
"times": [
0.67346204084,
0.68966477284,
0.6822361388400001,
0.6959907578400001,
0.72060276584,
0.7551644358400001,
0.69277382084,
0.7458905958400001,
0.7180951918400001,
0.7165229958400001
]
},
{
"command": "pnpr@main",
"mean": 0.7073868147400002,
"stddev": 0.08969702027882301,
"median": 0.68079027684,
"user": 0.37726216,
"system": 1.30058082,
"min": 0.6661453378400001,
"max": 0.9617794658400001,
"times": [
0.6822616698400001,
0.6865405628400001,
0.6746425008400001,
0.6661453378400001,
0.6816983618400001,
0.6798821918400001,
0.6717276858400001,
0.9617794658400001,
0.6926138618400001,
0.6765765088400001
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 3.09395541446,
"stddev": 0.03016694700289155,
"median": 3.09432818386,
"user": 1.9025015599999997,
"system": 1.9809387199999997,
"min": 3.05094482686,
"max": 3.1324522998599997,
"times": [
3.10124459086,
3.0660358298599997,
3.12147940286,
3.11024828586,
3.1324522998599997,
3.08170181686,
3.08741177686,
3.13149992586,
3.05653538886,
3.05094482686
]
},
{
"command": "pacquet@main",
"mean": 3.06060899366,
"stddev": 0.03394448419068206,
"median": 3.0513744983599995,
"user": 1.8323301599999997,
"system": 1.9866463200000002,
"min": 3.02660759286,
"max": 3.13011649586,
"times": [
3.05921912786,
3.0403151458599997,
3.11282446886,
3.0313060948599997,
3.02660759286,
3.05100883086,
3.0517401658599996,
3.13011649586,
3.05627882386,
3.04667318986
]
},
{
"command": "pnpr@HEAD",
"mean": 0.6855675983599999,
"stddev": 0.011076509220762686,
"median": 0.68560381486,
"user": 0.36308916,
"system": 1.3187819199999997,
"min": 0.66959577386,
"max": 0.70678043186,
"times": [
0.67325549886,
0.68693821686,
0.67839086586,
0.68766388186,
0.68664498286,
0.69899169986,
0.70678043186,
0.68285198486,
0.68456264686,
0.66959577386
]
},
{
"command": "pnpr@main",
"mean": 0.69880834786,
"stddev": 0.06851674717670805,
"median": 0.67684498036,
"user": 0.36801026000000003,
"system": 1.2933537199999998,
"min": 0.66876854886,
"max": 0.89225161186,
"times": [
0.67764536686,
0.69930247286,
0.68094253286,
0.67330922286,
0.67318128086,
0.66899248086,
0.66876854886,
0.89225161186,
0.67653215086,
0.67715780986
]
}
]
} |
|
| Branch | pr/12574 |
| Testbed | pacquet |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result milliseconds (ms) (Result Δ%) | Upper Boundary milliseconds (ms) (Limit %) |
|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 4,784.81 ms(+12.04%)Baseline: 4,270.51 ms | 5,124.61 ms (93.37%) |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot 🚷 view threshold | 3,093.96 ms(+2.27%)Baseline: 3,025.14 ms | 3,630.16 ms (85.23%) |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 1,376.86 ms(+3.35%)Baseline: 1,332.21 ms | 1,598.65 ms (86.13%) |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 4,399.44 ms(+11.37%)Baseline: 3,950.19 ms | 4,740.22 ms (92.81%) |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 645.97 ms(+4.72%)Baseline: 616.87 ms | 740.24 ms (87.26%) |
|
| Branch | pr/12574 |
| Testbed | pnpr |
⚠️ WARNING: No Threshold found!Without a Threshold, no Alerts will ever be generated.
Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the--ci-only-thresholdsflag.
Click to view all benchmark results
| Benchmark | Latency | milliseconds (ms) |
|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot | 2,820.32 ms |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot | 685.57 ms |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot | 709.04 ms |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot | 2,932.85 ms |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot | 715.25 ms |
Summary
Fixes GHSA-rp44-v426-6m56 / CAND-PNPM-034 in
pnpr(the Rust registry server).Bearer-token records carry
readonlyandcidr_whitelistrestrictions — they are persisted, loaded, and surfaced bynpm token list— but nothing ever enforced them during authorization. The token lookup on the auth hot path resolved a raw token to just its username and dropped the restrictions, so a token marked read-only or pinned to a network could still publish and mutate packages from anywhere.The fix
TokenBackend::lookup_record(a default method reusing each backend's existingfind_by_key) resolves the fullTokenRecord— username plus restrictions — so the local SQLite store and the libsql/sqlx backends all loadreadonlyandcidr_whitelistwith no per-backend change.Enforcement runs in an axum middleware layer ahead of every route handler, so a restricted token is rejected before a write handler buffers its (up to 100 MiB) request body:
403; GET/HEAD/OPTIONS and the resolver POSTs pass.PeerAddrConnectInfonewtype wired throughaxum::serve), never a client-suppliedX-Forwarded-For. CIDR matching is done in-house (no new dependency) and covers IPv4/IPv6,/0–/32//128, bare-host exact match, and IPv4-mapped IPv6 peers. A malformed entry or an unavailable peer fails closed.Single-resolution refactor
The same middleware is now the one place a request is authenticated: it resolves the
Authorizationheader to anIdentity(looking a bearer token up as a full record so the restrictions above are enforced there) and stashes it in request extensions. Handlers read it back through anAuthedCallerextractor instead of each re-inspecting the header. This removes the second token lookup authed requests used to pay (a real round-trip on the remote libsql backend) and the policy/identity race two independent lookups allowed; the per-handlerenforce_access/resolve_identity/caller_usernamehelpers collapse into the existing synchronousauthorize()and a purerequire_caller()check.Basic-auth, anonymous, and unknown/revoked tokens pass through unchanged and remain subject to the per-package access policy. A backing-store failure during lookup surfaces as a 5xx rather than silently skipping the check.
This is pnpr-only; it does not change the pnpm CLI or pacquet behavior.
Squash Commit Body
Checklist
pnpr/), which has no pnpm-CLI or pacquetcounterpart to mirror, so no TypeScript/pacquet port is needed.
pnpris a Rust crate, not a published npm package in thechangeset/release flow.
lookup_record, CIDR matching edge cases,method/header classification, end-to-end restriction enforcement, and that
the resolved identity reaches handlers).
Written by an agent (Claude Code, claude-opus-4-8).
Summary by CodeRabbit
Release Notes
New Features
Security / Access Control
Tests / Quality