I Found an Unauthenticated Attachment Disclosure Bug in a WordPress Support Plugin — and a…
Press enter or click to view image in full sizeDisclosure Notice: This research was conducted entire 2026-7-5 06:30:42 Author: infosecwriteups.com(查看原文) 阅读量:2 收藏

Press enter or click to view image in full size

Disclosure Notice: This research was conducted entirely in an isolated, locally-hosted Docker test environment running a fresh install of WordPress and the publicly available “latest-stable” release of the plugin in question, downloaded directly from the official WordPress.org plugin repository. No live, production, or third-party website was accessed, scanned, or tested at any point. All file contents shown are synthetic test data created solely for this research. The affected plugin’s name and the exact route are intentionally redacted here, because the underlying issue is currently being tracked through coordinated disclosure and may not yet be fully patched at the time of writing. This write-up is published strictly for educational purposes.

Background

Most of my CVE research starts from one theory: a plugin whose developers made one authorization mistake will usually have made others, and the categories that leak most often are the ones tied to user-owned objects — tickets, attachments, profiles, orders. Broken Access Control is, by a wide margin, the single most productive class in the WordPress plugin ecosystem, and unauthenticated variants sit at the top of that list.

This time the target was a support-desk / ticketing plugin — the kind of software where customers upload invoices, ID scans, contracts, and screenshots straight into a ticket. If the endpoint that serves those attachments doesn’t check who is asking, the impact isn’t abstract: it’s other people’s private documents.

What follows is a fully independent, fully reproducible finding — and the moment, after submission, when I learned it overlapped with a report already sitting in a vulnerability database’s pipeline. I’m publishing the technical breakdown anyway, because the methodology and the honest reconciliation with prior art are the actual point of doing this in public.

Scope & Method

  • Target: A WordPress support/ticketing plugin (redacted), latest-stable from WordPress.org
  • Environment: Local, isolated Docker stack — WordPress + MySQL 5.7
  • Assessment Type: White-box source audit + black-box PoC validation
  • Authorization: Self-authorized, isolated local research environment — no live targets
  • Tools: grep, WP-CLI, curl, docker, MySQL CLI

Phase 1: Target Confirmation

Before touching anything, I confirmed exactly what I was auditing: the plugin name, its version, that it was active, and the WordPress version underneath it. This is the first screenshot in every submission I make, because a reviewer needs to know the finding was validated against a real, current install — not a hypothetical.

=== TARGET CONFIRMATION ===
Plugin: <redacted> (latest-stable)
Version: <redacted — current release at time of testing>
Active: YES
WordPress: 7.0
Site URL: http://<local-docker>:8080

The critical detail here: I was testing the current version. Not an old release with a known history — the newest code the plugin ships today.

Phase 2: Mapping the Attack Surface

The plugin exposes its functionality through a REST namespace. I exported the source via SVN and mapped every route, paying special attention to the permission callbacks — the functions WordPress calls to decide whether a request is allowed before the handler runs.

grep -n "RegisterRestRoute\|permission" <source>/api/v1/<controller>.php

One route stood out immediately — the handler that serves ticket and reply file attachments:

$this->RegisterRestRoute(
'GET',
'file-dl/(?P<type>[a-zA-Z0-9-]+)/(?P<id>[0-9_]+)/(?P<file>[^/]+)',
[$this, "file_dl"]
);

Three attacker-controlled segments — a type selector, a numeric identifier, and a filename — feeding a file-download handler. Exactly the shape of an IDOR, if the permission gate is weak. So I read the gate.

Phase 3: Root Cause

The route’s permission logic resolved, for this particular download route, to a single unconditional line:

} elseif ($route == "file-dl") {
return true;
}

That’s the whole bug. The permission callback returns true for the attachment-download route unconditionally — no authentication check, no nonce, no verification that the requester owns the ticket the file belongs to. Once that callback returns true, WordPress hands the request straight to the download handler, which reads the identifier and filename from the URL and returns the file.

Because the callback never looks at the current user, there is no notion of “your ticket” versus “someone else’s ticket.” Every attachment is reachable by everyone — including an anonymous visitor with no account at all.

Press enter or click to view image in full size

Phase 4: Building an Isolated Test Environment

To prove impact safely, I stood up a throwaway install rather than touching any live site: WordPress + MySQL 5.7 in Docker, the plugin installed from the dashboard, and a realistic victim scenario seeded by hand.

Get Shikhali Jamalzade’s stories in your inbox

Join Medium for free to get updates from this writer.

Remember me for faster sign in

I created two synthetic victim artifacts, standing in for what a real customer would attach:

  • A ticket attachment (type = T) containing a fake "confidential customer record."
  • A reply attachment (type = R) containing a fake "private invoice."
=== SETUP: victim ticket + reply attachments ===
[ticket attachment created — synthetic "customer record"]
[reply attachment created — synthetic "invoice"]
Files created: 2

I also inserted the matching reply row into the plugin’s database table, because the reply-download path validates that a reply record exists before serving its file. This made the second attack vector reachable exactly as it would be on a real site.

Phase 5: Proof of Concept

Vector 1 — Unauthenticated Ticket Attachment (type = T)

From a session with no cookies, no auth header, no login, I requested the ticket attachment and filtered the output to show that the request carried no credentials and the server returned the file anyway:

> GET /wp-json/<plugin>/v1/ticket/file-dl/T/1/<file> HTTP/1.1
> Host: <local-docker>
< HTTP/1.1 200 OK
[SYNTHETIC CONFIDENTIAL RECORD RETURNED]

No Cookie header. No Authorization header. HTTP 200, and the full attachment content in the response body.

Press enter or click to view image in full size

Vector 2 — Unauthenticated Reply Attachment (type = R)

The reply path uses a compound {ticketId}_{replyId} identifier. Same anonymous session, same result:

> GET /wp-json/<plugin>/v1/ticket/file-dl/R/1_1/<file> HTTP/1.1
> Host: <local-docker>
< HTTP/1.1 200 OK
[SYNTHETIC PRIVATE INVOICE RETURNED]

Two independent download paths, both fully unauthenticated.

Integrity Proof

A 200 response proves the endpoint answered — but I wanted to prove the anonymous request returned the actual victim file, byte for byte, not a placeholder or an error page. So I compared the MD5 of the file on disk with the MD5 of what the unauthenticated request pulled down:

--- [A] File on server (victim's attachment) ---
254e7a2a21c6d0d55fbc11fc08e30c18 <server-side file>
--- [B] Content retrieved via unauthenticated request ---
254e7a2a21c6d0d55fbc11fc08e30c18 <downloaded file>

Identical hashes. Byte-for-byte exfiltration, from an anonymous session, confirmed.

Press enter or click to view image in full size

Why This Scales

The identifiers are sequential integers. An attacker doesn’t need to guess — they increment. Combined with the fact that support tickets routinely carry personal data, invoices, and contracts, and that the plugin’s upload whitelist covers pdf, doc/docx, xls/xlsx, txt, and common image formats, a single unauthenticated loop over the ID space harvests attachments across every customer on the site.

Estimated severity: CVSS 3.1 7.5 (High)AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N. Network-reachable, no privileges, no interaction, high confidentiality impact.

The Reality Check

Before public disclosure I did what I always do now: I checked the vulnerability databases and I contacted the vendor.

The vendor email went out first — a responsible-disclosure notice with a summary of the issue and a request for a secure contact, deliberately without the full PoC in the first message. Then I submitted the finding to a CNA with the complete technical detail and requested a CVE.

The response was: duplicate.

Not a duplicate of the plugin’s older, public authorization issues — those were a different, integrity-only problem on a different function. This was a duplicate of a separate report already in the CNA’s pipeline, covering exactly this unauthenticated attachment-download route and exactly this “permission callback returns true” root cause, already tracked with the confidentiality impact of returning full attachment contents to anonymous callers.

Someone had gotten there first, by a matter of weeks, into a queue I couldn’t see.

Press enter or click to view image in full size

What I Was Told — and What It’s Worth

Here’s the part that turned a rejection into something genuinely useful. The existing record was filed against an earlier version, and marked fixed in a later one. My finding reproduced on the current release — the one that was supposed to be patched.

The reviewer’s response was precise, and I’m quoting the substance of it because it reframed the whole finding for me: my confirmation that the issue still reproduces on the current version, together with the byte-for-byte MD5 proof, would be used to extend the affected-version range on the existing entry beyond the version it was originally filed against. Because it’s the same vulnerability and the same code path, it’s handled under the existing record rather than as a separate CVE.

So: no CVE with my name on it. But my independent reproduction demonstrated that a fix believed to close the issue did not, and that correction lands in the public record where it actually protects people. That’s not nothing. That’s the point of the work.

I want to be precise about what I’m claiming and what I’m not. I did not discover a novel bug here — I independently rediscovered a known one and proved it was still live where it was believed dead. The value isn’t novelty; it’s verification. Those are different contributions, and conflating them would be dishonest.

Press enter or click to view image in full size

Attack Chain Summary

[Attacker — no credentials, no prior session]


[1] Map REST routes; find attachment-download handler


[2] Read permission callback → returns true unconditionally for file-dl


[3] Seed victim ticket + reply attachments in isolated Docker install


[4] GET file-dl/T/<id>/<file> → HTTP 200, ticket attachment (no auth)


[5] GET file-dl/R/<id>/<file> → HTTP 200, reply attachment (no auth)


[6] MD5(server file) == MD5(downloaded file) → byte-for-byte exfiltration


[7] Sequential IDs → enumerate → harvest attachments across all tickets

What This Taught Me

A “fixed in X” label is a claim, not a guarantee. The most valuable thing I did in this entire audit was test the current version instead of assuming the changelog was true. The issue was marked fixed; it wasn’t. Independent reproduction against the latest release is how that gets caught.

Duplicate-by-pipeline is invisible until it isn’t. I checked every public database before submitting, and it was clean — because the report that duplicated mine wasn’t public yet. You cannot fully de-risk this. What you can do is target less-crowded plugins: the more popular the software, the more researchers are already circling it. Two of my findings that week collided with pipeline reports; both were popular plugins. The niche ones didn’t collide.

Precision about your own contribution is a security skill. “I found a new bug,” “I independently rediscovered a known bug,” and “I proved a known bug wasn’t actually fixed” are three different sentences with three different truth values. Picking the correct one — especially when the flattering one is right there — is part of doing this honestly.

The process transfers regardless of the outcome. Standing up an isolated environment, tracing an unauthenticated entry point to confirmed impact, building two independent PoCs, proving exfiltration with a hash rather than a screenshot alone — that skill set is identical whether the audit ends in a CVE or a “thanks, we’ll extend the range.”

If you found this useful, feel free to connect on LinkedIn or check out my tools on GitHub.

All testing was conducted in an isolated, locally-hosted environment using a publicly available plugin release. No live or third-party systems were accessed at any point during this research. The plugin name and exact route are redacted pending completion of coordinated disclosure.


文章来源: https://infosecwriteups.com/i-found-an-unauthenticated-attachment-disclosure-bug-in-a-wordpress-support-plugin-and-a-435e86868d04?source=rss----7b722bfd1b8d---4
如有侵权请联系:admin#unsafe.sh