WordPress · Report #4043537
CampTix [camptix_private]: the view token is derived from the requester's own IP, so naming any attendee's e-mail unlocks the gated content
How it was found
Description:
[camptix_private] hides page content behind a check that the visitor holds a ticket. The check is a
"view token" that the plugin both issues and verifies from data the requester controls:
// plugins/camptix/addons/shortcodes.php:691
public function generate_view_token_for_attendee( $attendee_id ) {
$email = get_post_meta( $attendee_id, 'tix_email', true );
$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '';
$view_token = md5( 'tix-view-token-' . strtolower( $email . $ip ) );
return $view_token;
}
The POST handler runs on template_redirect with no nonce (the file disables the sniff at line 441)
and sends nothing to the address it is given — no confirmation mail, no magic link:
// shortcodes.php:454
if ( isset( $_POST['tix_private_shortcode_submit'] ) ) {
$email = isset( $_POST['tix_email'] ) ? trim( stripslashes( $_POST['tix_email'] ) ) : '';
...
$attendees = get_posts( array( 'post_type' => 'tix_attendee', 'post_status' => 'publish',
'meta_query' => array( array( 'key' => 'tix_email', 'value' => $email ) ) ) );
if ( $attendees ) {
$attendee = $attendees[0];
$view_token = $this->generate_view_token_for_attendee( $attendee->ID ); // uses the REQUESTER's IP
setcookie( 'tix_view_token', $view_token, ... );
$_COOKIE['tix_view_token'] = $view_token; // forced into this same request
foreach ( $attendees as $attendee ) {
update_post_meta( $attendee->ID, 'tix_view_token', $view_token ); // overwrites the real holder's token
...
The gate then re-derives the expected token the same way and compares:
// shortcodes.php:568
$expected_view_token = $this->generate_view_token_for_attendee( $attendee->ID );
if ( $expected_view_token !== $view_token ) {
$camptix->error( __( 'Looks like you logged in from a different computer. Please log in again.' ) );
Both sides of that comparison are computed from the same request, by the same IP — the attacker's. The IP was meant to bind the token to the ticket holder's device; it binds it to whoever is asking. The only thing the check actually requires is knowing an attendee's e-mail address.
That address is not a secret. It is on sponsor and speaker pages, in public mailing lists, in a forwarded confirmation mail, or simply guessable for a corporate address format.
Steps To Reproduce:
CampTix 2.0.0 on WordPress 7.1.1. evidence/03-poc.sh produced evidence/01-transcript.txt.
(evidence/04-wordcamp-platform-stubs.php only stubs get_wordcamp_post() so CampTix loads off the
wordcamp.org platform; it touches nothing in the code path under test.)
-
A page contains
[camptix_private]...gated content...[/camptix_private], and an attendee record exists for[email protected]. -
Control A — anonymous GET, no POST:
occurrences of the gated marker: 0The gate works.
-
Control B — POST naming an address with no attendee record:
POST tix_private_shortcode_submit=1&[email protected] occurrences of the gated marker: 0A real attendee address is genuinely required. This is what makes step 4 an authentication failure rather than a blanket bypass.
-
Attack — same client, no cookie, no nonce, no account, naming the victim's address:
POST tix_private_shortcode_submit=1&[email protected] occurrences of the gated marker: 1 CTPRIVATE8B3F attendees-only content -
Side effect on the legitimate holder. The same request rewrote the attendee's stored token:
tix_view_token was : ORIGINAL-TOKEN-OF-THE-REAL-ATTENDEE tix_view_token now : cbe2030c3295c959657716b654db0674The real ticket holder's cookie no longer matches, so on their next visit they are told "Looks like you logged in from a different computer. Please log in again."
Recommendations
The token must be unguessable by the person requesting it, and requesting it must prove control of the inbox:
- Generate the token from
wp_generate_password( 32, false )(orwp_generate_uuid4()) and store it; never derive it from data the requester supplies. - Mail the resulting link to the address instead of unlocking the content in the same response. The confirmation mail is the ownership proof that is missing today.
- Add a nonce to the form and rate-limit submissions per IP, so the handler cannot be driven as an oracle for which addresses hold tickets.
- Do not overwrite
tix_view_tokenfor every attendee sharing the address on each submission — issue per-session tokens, so one request cannot evict an existing holder.
Point 2 alone closes the disclosure. Point 4 is what stops the denial of access to the real holder.
DISCLOSURE
I will refrain from sharing information about this issue for 120 calendar days after your acknowledgement of receipt, per your vulnerability disclosure policy.
Impact
Anyone who knows an attendee's e-mail address reads content a WordCamp organiser restricted to ticket holders — attendee-only schedules, venue or access details, private video links, sponsor material, after-party or accommodation information. No account, no ticket, no cookie, no nonce: one POST.
The e-mail address is the only thing the check really asks for, and it is routinely obtainable. It appears on speaker and sponsor pages, in public mailing lists and in forwarded confirmation mails, and for most organisations it follows a predictable format. The handler also answers differently for an address that has a ticket and one that does not, so it doubles as an oracle for "did this person attend", which is itself information the attendee did not publish.
The same request takes access away from the person who paid for it. It overwrites tix_view_token
on every attendee record sharing that address, so the legitimate holder's cookie stops matching and
they are told they "logged in from a different computer". An attacker who repeats the request keeps
them locked out indefinitely, and the plugin records only the attacker's IP against the attendee.
Limits, stated plainly:
- The camp must use
[camptix_private]. The shortcode is an optional CampTix addon that an organiser places on a page. Sites that never use it are unaffected, and I did not survey how many WordCamp sites do — this is why I have scoredAT:P. - The attacker must know or guess a real attendee address. Control B in the transcript shows an address with no attendee record discloses nothing.
- Only the gated block is disclosed. Ticket records, payment data and the attendee list itself are not returned by this path.
- Verified on a local install of CampTix 2.0.0, not against a production WordCamp site. I did not test this against anyone's real event, because doing so would mean reading content gated for real attendees using a real person's address.
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N = 6.3 Medium.
AT:P because the vulnerable path exists only where the shortcode is deployed. VI:L for the token
overwrite that evicts the legitimate holder. Scored without the shortcode precondition (AT:N) it
would be 6.9; I have taken the lower reading.