Skip to content

CVE-2026-62382: Verified Reproduction

CVE-2026-62382: PasswordPusher allows unauthenticated deletion of anonymous pushes due to a nil==nil ownership check that bypasses viewer-deletion restrictions.

CVE-2026-62382 is verified against pglombardo/PasswordPusher · Ruby on Rails self-hosted application, also shipped as Docker image pglombardo/pwpush. Affected versions: v1.45.11 through v2.9.5. Fixed in v2.9.6. This medium reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00328.

REPRO-2026-00328 pglombardo/PasswordPusher · Ruby on Rails self-hosted application, also shipped as Docker image pglombardo/pwpush Aug 23, 2026 CVE entry .txt
Severity
MEDIUM
CVSS
6.9
Confidence
HIGH
Reproduced in
21m 33s
Tool calls
131
Spend
$2.32
01 · Overview

What Is CVE-2026-62382?

CVE-2026-62382 is a medium-severity vulnerability affecting pglombardo/PasswordPusher v1.45.11 through v2.9.5. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00328).

02 · Severity & CVSS

CVE-2026-62382 Severity & CVSS Score

CVE-2026-62382 is rated medium severity, with a CVSS base score of 6.9 out of 10.

MEDIUM threat level
6.9 / 10 CVSS base
Weakness CWE-863 Incorrect Authorization — Incorrect Authorization

Medium — meaningful risk under specific conditions. Schedule a fix in the normal cycle.

03 · Affected Versions

Affected pglombardo/PasswordPusher Versions

pglombardo/PasswordPusher · Ruby on Rails self-hosted application, also shipped as Docker image pglombardo/pwpush versions v1.45.11 through v2.9.5 are affected.

How to Reproduce CVE-2026-62382

$ pruva-verify REPRO-2026-00328
or curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00328/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-62382

Authorization bypass — reproduced
  • reached the target end-to-end
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

url_token path parameter of an unauthenticated DELETE /p/<url_token>.json request

Attack chain
  1. Api::V1::PushesController#destroy (and PushesController#expire for HTML) via (@push.user == current_user) || @push.deletable_by_viewer where nil == nil
Runnable proof: reproduction_steps.sh
Captured evidence: pwpush fixed service
How the agent worked 277 events · 131 tool calls · 21 min
21 minDuration
131Tool calls
52Reasoning steps
277Events
6Dead-ends
Agent activity over 21 min
Policy
1
Support
10
Repro
110
Judge
31
Variant
120
Verify
1
0:0021:24

Root Cause and Exploit Chain for CVE-2026-62382

Versions: v1.45.11 – v2.9.5 (fixed in v2.9.6). Only deployments

flaw (CWE-863) in the push-deletion paths. Both the JSON API (Api::V1::PushesController#destroy) and the HTML UI (PushesController#expire) authorize deletion with (@push.user == current_user) || @push.deletable_by_viewer. For an anonymous push @push.user is nil, and for an unauthenticated request current_user is also nil, so the ownership comparison evaluates nil == niltrue. The deletable_by_viewer restriction is therefore bypassed, and anyone who knows only the secret URL can permanently delete (expire!) an anonymous push — clearing payload, passphrase, and attached files — even when viewer deletion was explicitly disabled and a passphrase protects reads.

pglombardo/pwpush Docker images.

  • Affected versions: v1.45.11 – v2.9.5 (fixed in v2.9.6). Only deployments allowing anonymous pushes (the default) are affected.
  • Risk: Medium (CVSS 4.0: 6.9). Unauthenticated denial-of-service against secrets in transit: an attacker who learns or guesses a secret URL token can irreversibly destroy the push before the intended recipient retrieves it.

Impact Parity

  • Disclosed/claimed maximum impact: authorization bypass — unauthenticated deletion of anonymous pushes (authz_bypass).
  • Reproduced impact from this run: identical. An unauthenticated DELETE /p/<url_token>.json against pwpush 2.9.5 returned HTTP 200, set expired=true/deleted=true, cleared the passphrase, and destroyed the payload (subsequent authorized read with the correct passphrase returned "payload": null). The HTML route DELETE /p/<url_token>/expire also destroyed the push (HTTP 302 + push expired).
  • Parity: full.
  • Not demonstrated: nothing claimed beyond the authorization bypass / data destruction (no code execution was claimed or attempted).

Root Cause

In v2.9.5 the deletion guards were:

  • app/controllers/api/v1/pushes_controller.rb (destroy): if (@push.user == current_user) || @push.deletable_by_viewer
  • app/controllers/pushes_controller.rb (expire): unless @push.deletable_by_viewer || (@push.user == current_user)

Push#user is a nullable belongs_to. Anonymous pushes have user_id = NULL, so @push.user is nil. Devise's current_user is nil when the request is unauthenticated. Ruby evaluates nil == nil as true, so the "owner" branch succeeds and the deletable_by_viewer check is never reached. expire! then clears payload, passphrase, and files and marks the push expired/deleted — irreversible.

Fix (v2.9.6, diff v2.9.5...v2.9.6): both controllers now call a new model method Push#deletable_by?(user):

def deletable_by?(user)
  (user.present? && user_id == user.id) || deletable_by_viewer == true
end

which requires an authenticated (present?) user whose id matches the owner, removing the nil==nil equivalence. Advisory: https://github.com/pglombardo/PasswordPusher/security/advisories/GHSA-jf2m-hpj9-4qx2

Reproduction Steps

  1. bundle/repro/reproduction_steps.sh (self-contained; requires Docker).
  2. The script:
    • Pulls and starts the real product images pglombardo/pwpush:2.9.5 (vulnerable, port 15100) and pglombardo/pwpush:2.9.6 (fixed, port 15101).
    • Waits for HTTP readiness, then completes the real first-run setup flow (extracts the one-time boot code from container logs and creates the admin account), mirroring a fresh deployment.
    • As an unauthenticated client, creates an anonymous push with payload=SUPER-SECRET-CVE-2026-62382, passphrase=s3cr3t, deletable_by_viewer=false.
    • Verifies the payload is unreadable without the passphrase (HTTP 401).
    • Sends DELETE /p/<url_token>.json with no session and no passphrase.
    • Re-reads the push with the correct passphrase and evaluates state.
    • Repeats the identical flow against the fixed image as a negative control, and additionally exercises the HTML DELETE /p/<token>/expire route on the vulnerable instance as secondary evidence.
  3. Expected evidence: vulnerable → DELETE HTTP 200, push expired=true, deleted=true, payload=null; fixed → DELETE HTTP 401 ("That push is not deletable by viewers."), payload intact.

Evidence

  • bundle/logs/reproduction_steps.log — full run transcript. Key excerpts:

    [repro] [vuln] read without passphrase -> HTTP 401
    [repro] [vuln] unauthenticated DELETE /p/cxlwytxdhjwx.json -> HTTP 200
    [repro] [fixed] unauthenticated DELETE /p/5hpxxmf2x6_unhxtmq.json -> HTTP 401
    [repro] vuln:  DELETE=200 expired=true deleted=true payload=null
    [repro] fixed: DELETE=401 expired=false payload=SUPER-SECRET-CVE-2026-62382
    [repro] RESULT: CVE-2026-62382 CONFIRMED (vuln exploited, fixed rejected).
    
  • bundle/artifacts/http/vuln_delete_response.json — vulnerable DELETE response body: expired:true, deleted:true, passphrase:null.

  • bundle/artifacts/http/vuln_read_after_delete.json — authorized read after the attack returns "payload": null (secret destroyed).

  • bundle/artifacts/http/vuln_read_without_passphrase.json — pre-attack 401 proves the passphrase gate was active.

  • bundle/artifacts/http/vuln_html_expire_response.txt — HTML route also expires the push unauthenticated.

  • bundle/artifacts/http/fixed_delete_response.json401 + {"error":"That push is not deletable by viewers."}.

  • bundle/artifacts/http/fixed_read_after_delete.json — payload intact on fixed version.

  • bundle/logs/pwpush_vuln_service.log / bundle/logs/pwpush_fixed_service.log — container logs (Puma boot, first-run, request handling).

  • bundle/repro/runtime_manifest.json — runtime manifest with image digests and SHA-256 of every proof artifact.

  • Environment: Docker 29.1.3 on Linux x86_64; images pglombardo/pwpush:2.9.5 (sha256:ba5cf45b…) and pglombardo/pwpush:2.9.6 (sha256:c9662425…), Ruby 4.0.6 / Rails 8.1.3.1, production environment with default settings (allow_anonymous enabled).

Recommendations / Next Steps

  • Upgrade to PasswordPusher ≥ v2.9.6 (or apply the Push#deletable_by? patch).
  • Never use record.user == current_user as an ownership test when either side can be nil; require current_user.present? && record.user_id == current_user.id.
  • Add regression tests: unauthenticated DELETE/expire of an anonymous push with deletable_by_viewer=false must be rejected (the fix release adds test/integration/password/password_json_deletion_test.rb etc.).
  • Defense-in-depth: deployments that do not need anonymous pushes should set allow_anonymous: false; secret URL tokens should be treated as bearer secrets and rotated.

Additional Notes

  • Idempotency: the script removes/recreates its containers (pwpush-vuln-repro, pwpush-fixed-repro) on each run, performs first-run setup from scratch, and exits 0 only when the vulnerable instance is exploited AND the fixed instance rejects the attack. Verified passing twice consecutively in this run.
  • First-run admin setup is required by current PasswordPusher images before any push can be created; the script automates it via the boot code printed to container logs. This reflects real deployment behavior and does not affect the unauthenticated attack surface.
  • Limitations: the boot-code extraction depends on the Docker log driver; a non-Docker deployment would need the equivalent first-run step. The claimed entrypoint (DELETE /p/<url_token>.json) is exercised directly; the HTML /expire route is included only as secondary evidence.

CVE-2026-62382 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:001:35
0:00
session startedaccounts/fireworks/models/kimi-k3 · CVE-2026-62382 · REPRO-20
0:08
0:09
0:10
web search
0:41
0:41
extract_facts
no facts extracted
0:44
0:44
0:45
supportclaim_contract
0:52
0:52
0:52
0:57
0:58
0:58
0:58
1:04
1:04
$docker --version 2>&1; docker ps 2>&1 | head -3; which ruby python3 curl jq 2>&1; cat /workspace/bundle/.pruva/run_attempt.json
0.3s
Docker version 29.1.3, build 29.1.3-0ubuntu4.1
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
/usr/bin/python3
/usr/bin/curl
/usr/bin/jq
{
  "schema_version": 1,
  "run_id": "b8267de6-dfe1-4257-b9ae-b6d58800dfb5",
1:12
1:13
1:19
1:20
$cd /tmp && curl -sL https://api.github.com/repos/pglombardo/PasswordPusher/compare/v2.9.5...v2.9.6 -o diff.json && jq -r '.files[].filename' diff.json | head -30
0.8s
VERSION
app/controllers/api/v1/pushes_controller.rb
app/controllers/pushes_controller.rb
app/models/push.rb
test/integration/api/api_v2_pushes_test.rb
test/integration/password/password_deletion_test.rb
test/integration/password/password_json_deletion_test.rb
test/models/push_edit_test.rb
1:22
$cd /tmp && jq -r '.files[] | select(.filename | test("controllers")) | "=== " + .filename + " ===\n" + .patch' diff.json | head -120
0.2s
=== app/controllers/api/v1/pushes_controller.rb ===
@@ -308,7 +308,7 @@ def audit
     https://docs.pwpush.com/docs/json-api/
   EOS
   def destroy
-    if (@push.user == current_user) || @push.deletable_by_viewer
+    if @push.deletable_by?(current_user)
       unless @push.expired?
1:29
$cd /tmp && jq -r '.files[] | select(.filename == "app/models/push.rb") | .patch' diff.json
0.3s
@@ -193,6 +193,13 @@ def expire!
     save!
   end
 
+  # True when +user+ is the authenticated owner, or when viewer deletion is
+  # explicitly enabled. Anonymous pushes have a nil owner; comparing two nils
+  # must not count as ownership.
+  def deletable_by?(user)
1:32
08 · How to Fix

How to Fix CVE-2026-62382

Upgrade pglombardo/PasswordPusher · Ruby on Rails self-hosted application, also shipped as Docker image pglombardo/pwpush to v2.9.6 or later.

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-62382 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-62382

Is CVE-2026-62382 exploitable?

Yes. Pruva independently reproduced CVE-2026-62382 in pglombardo/PasswordPusher and verified the exploit fires end-to-end in a sandboxed environment. A runnable proof-of-concept script and the full agent transcript are on this page (reproduction REPRO-2026-00328).

How severe is CVE-2026-62382?

CVE-2026-62382 is rated medium severity, with a CVSS score of 6.9 out of 10.

What type of vulnerability is CVE-2026-62382?

CVE-2026-62382 is classified as CWE-863 Incorrect Authorization (Incorrect Authorization).

Which versions of pglombardo/PasswordPusher are affected by CVE-2026-62382?

pglombardo/PasswordPusher v1.45.11 through v2.9.5 is affected by CVE-2026-62382.

Is there a fix for CVE-2026-62382?

Yes. CVE-2026-62382 is fixed in pglombardo/PasswordPusher v2.9.6. Upgrading to the fixed version remediates the issue.

How can I reproduce CVE-2026-62382?

Pruva provides a verified reproduction script on this page. Download it and run it inside an isolated environment such as a container or virtual machine — never against production. The reproduction was confirmed end-to-end by Pruva's automated agents.

Is the CVE-2026-62382 reproduction verified?

Yes. Pruva reproduced CVE-2026-62382 with high confidence in a sandboxed environment, capturing the full agent transcript and artifacts as evidence.
11 · References

References for CVE-2026-62382

Authoritative sources for CVE-2026-62382 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.