HexBrain

WordPress · Report #3871816

Unauthenticated private CSV export of Meetup organizer PII via broken authz check in WordCamp Reports

Validstatus
highseverity
16upvotes
5 Nov 2026disclosed

Summary

An inverted authorization check in WordCamp Reports (Meetup_Details::export_to_file()) used && instead of || between nonce verification and the view_wordcamp_reports capability check. Unauthenticated requests with an invalid nonce could export a private CSV of Meetup organizer fields (public => false).

How it was found

Summary

Broken authorization check in the WordCamp Reports plugin allows an unauthenticated attacker to export a private CSV containing Meetup organizer PII (email, postal address, Slack handles, usernames, notes) from central.wordcamp.org.

The bug is a logic error in Meetup_Details::export_to_file(): the security gate uses && instead of ||, so the export is blocked only when the request has an invalid nonce AND the user already has the privileged capability. Unauthenticated / low-privilege users therefore pass the check.

This is not by design. Sibling report classes in the same plugin correctly require both a valid nonce and the view_wordcamp_reports capability. The export intentionally sets 'public' => false to include private organizer fields.

Affected Asset / Component

  • Program asset: WordCamp.org
  • Host: https://central.wordcamp.org
  • Component: wordcamp-reports plugin
  • File: public_html/wp-content/plugins/wordcamp-reports/classes/report/class-meetup-details.php
  • Function: Meetup_Details::export_to_file() (hooked to admin_init)
  • Intended capability: view_wordcamp_reports

Vulnerable Code

Broken check in class-meetup-details.php:

if ( ! wp_verify_nonce( $nonce, 'run-report' ) && current_user_can( CAPABILITY ) ) { return; }

$options = array( 'fields' => $fields, 'public' => false, // includes private organizer fields );

Correct pattern used by sibling reports (example: class-meetup-sponsors.php):

if ( ! wp_verify_nonce( $nonce, 'run-report' ) || ! current_user_can( CAPABILITY ) ) { return; }

Truth table of the broken condition

Nonce validHas view_wordcamp_reportsEarly return?Export proceeds?
NoNo (anonymous)NoYes (vulnerable)
NoYesYesNo
YesNoNoYes (vulnerable)
YesYesNoYes (intended)

Only privileged users with a bad nonce are blocked. Everyone else, including unauthenticated attackers, can trigger the private CSV export.

Private fields available when 'public' => false include organizer meta such as Email, Address, Slack, Organizer Name, Extra Comments, WordPress.org usernames, etc. (get_private_data_fields() + Meetup_Admin::meta_keys('organizer')).

The plugin documentation explicitly states private data must not be exposed publicly.

Steps To Reproduce

Preconditions: None. No authentication required.

  1. As an unauthenticated user (incognito / no cookies), send a POST request to WordCamp Central admin that fires admin_init (for example admin-ajax.php):

POST /wp-admin/admin-ajax.php HTTP/1.1 Host: central.wordcamp.org Content-Type: application/x-www-form-urlencoded

action=Export%20CSV&meetup-details-nonce=invalid&fields%5B%5D=Email&fields%5B%5D=Address&fields%5B%5D=Slack&fields%5B%5D=Organizer%20Name&fields%5B%5D=Extra%20Comments&fields%5B%5D=Name

Example curl (use only for minimal confirmation; do not mass-exfiltrate data):

curl -i -X POST 'https://central.wordcamp.org/wp-admin/admin-ajax.php'
-H 'Content-Type: application/x-www-form-urlencoded'
--data-urlencode 'action=Export CSV'
--data-urlencode 'meetup-details-nonce=invalid'
--data-urlencode 'fields[]=Email'
--data-urlencode 'fields[]=Address'
--data-urlencode 'fields[]=Slack'
--data-urlencode 'fields[]=Organizer Name'
--data-urlencode 'fields[]=Name'

  1. Observe that the broken check does not return early for an unauthenticated requester with an invalid nonce.

  2. Observe the response is a CSV download generated by export_to_file_common() containing private organizer fields (not limited to public report fields).

  3. Control comparison (proves intent): repeat the same request pattern against another report export in the same plugin that uses the correct || check (for example Meetup Sponsors). That export should refuse unauthenticated requests. Only Meetup Details is broken.

  4. Optional code confirmation: in the public repo WordPress/wordcamp.org (branch production), inspect class-meetup-details.php around the export_to_file() nonce/capability check and compare with class-meetup-sponsors.php.

Recommendations

Replace the inverted condition with the same pattern used by sibling reports:

if ( ! wp_verify_nonce( $nonce, 'run-report' ) || ! current_user_can( CAPABILITY ) ) { return; }

Also recommended:

  1. Audit all export_to_file() handlers in wordcamp-reports for the same operator mistake.
  2. Prefer fail-closed helpers (for example early current_user_can + check_admin_referer / wp_die on failure).
  3. Add a regression test: unauthenticated POST with action=Export CSV must not emit CSV content.

Impact

Unauthenticated attackers can download a private CSV containing Meetup organizer personally identifiable information (emails, physical addresses, Slack handles, WordPress.org usernames, and free-text notes) from WordCamp Central.

This is caused by an inverted authorization check (&& instead of ||) in Meetup_Details::export_to_file(). The intended access control requires both a valid nonce and the view_wordcamp_reports capability, but the broken condition allows anonymous users with an invalid nonce to bypass both checks.

Impact:

  • Confidentiality breach of volunteer/organizer PII managed by WordCamp.org
  • No authentication required
  • No valid nonce required
  • Low attack complexity (single HTTP POST)
  • Potential CSRF abuse against browsers that can reach wp-admin
  • Violates the plugin's own private/public data separation model ('public' => false exports)

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original