A free school-project PHP app dropped on GitHub with a textbook SQL injection, dressed up with a 7.3
SourceCodester's *Class and Exam Timetabling System* v1.0 is a free PHP/MySQL learning project distributed on sourcecodester.com — the kind of codebase students download to learn CRUD. CVE-2026-13485 is an unauthenticated SQL injection in /preview.php via the course_year_section parameter, which is concatenated directly into a query with no sanitization. The bug class is canonical CWE-89 (the CWE-74 label is the parent injection bucket). The same researcher set has filed near-identical SQLi CVEs against preview6.php, preview7.php, archive.php, and others in the same codebase (CVE-2026-13486, -13488, etc.) — the entire project is riddled with the same anti-pattern.
Vendor severity of 7.3 (HIGH) is a mechanical CVSS output, not a reflection of risk. The vector AV:N/AC:L/PR:N/UI:N is technically accurate — anyone who can reach the page can dump the DB — but the population that actually runs this software in production is a rounding error. There is no enterprise install base, no vendor patch pipeline, and no support contract. Treating this like an Oracle or SAP SQLi distorts the patch queue. The honest call is LOW: real, exploitable, public PoC, but blast radius bounded to a handful of student / hobbyist deployments.
4 steps from start to impact.
Locate an exposed instance
inurl:preview.php course_year_section) or Shodan/FOFA HTML title match. The application has no telemetry; instances are usually on shared LAMP hosting or a lab VM.- Instance is reachable from attacker's network position
- Default PHP routing exposes /preview.php
- Effectively zero enterprise deployment footprint
- Most installs are on localhost / classroom LAN, not internet-exposed
Inject into course_year_section
GET /preview.php?course_year_section=1' UNION SELECT ...-- - using sqlmap (sqlmap -u 'http://target/preview.php?course_year_section=1' --dbs). The parameter is concatenated unsafely into a MySQL query, so UNION-based and boolean-based payloads both work. PoC is public on VulDB and GitHub issue trackers.- No WAF in front of the app
- MySQL backend with default error verbosity
- Any generic WAF (ModSecurity CRS, Cloudflare free tier) catches the canonical sqlmap signatures
- Hosting providers commonly block UNION SELECT patterns at the edge
Exfiltrate schema and data
information_schema.tables, dumps user/account tables, and harvests credentials. If MySQL is running as a privileged account with FILE privilege and secure_file_priv is unset, sqlmap's --os-shell can write a PHP webshell into the docroot.- DB user has SELECT on target tables (default in this app)
- FILE privilege + writable webroot for shell upload
- Modern MySQL defaults restrict
secure_file_priv, blocking file write - Shared hosting separates DB user from webroot ownership
Impact bounded to one tenant
- Successful DB compromise from step 3
- No corporate identity integration to abuse
- Database is single-app, single-host by design
sourcecodester, preview.php, class-and-exam-timetabling. If zero hits, close the ticket. If hits exist, proceed. There is no mitigation SLA at LOW severity — handle within the 365-day remediation window./preview*.php, /archive.php at minimum.- Waiting for a vendor patch — SourceCodester does not ship CVE-keyed patches for community uploads.
- Input validation in the calling page alone — sibling CVEs show the same anti-pattern across the whole codebase, so per-file fixes leave you exposed.
- Database least-privilege as a primary control — it reduces blast radius but the C:L/I:L/A:L impact still lands; do it, but don't call it remediation.
The supporting signals.
| In-the-wild exploitation | None observed. No campaigns tracked. EPSS 0.00412 (≈0.4%) reflects negligible predicted exploitation. |
|---|---|
| Proof-of-concept | Public PoC on VulDB and researcher GitHub issues (lffaker/cybersec, yan-124/yan). Trivial sqlmap one-liner. |
| EPSS | 0.00412 — bottom of the distribution. Realistic given the audience. |
| KEV status | Not listed. Will not be — CISA does not catalog SourceCodester student-project bugs. |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L → 7.3 HIGH. CVSS v4.0 reassessed to 6.9 MEDIUM, closer to reality. |
| CWE | CWE-89 (SQL Injection) — parent label CWE-74 used in NVD entry. Classic concatenation flaw. |
| Affected versions | SourceCodester Class and Exam Timetabling System v1.0 (only published version). Codebase has sibling CVEs: -13484, -13486, -13487, -13488. |
| Fixed version | None. SourceCodester does not maintain a patch pipeline for community-uploaded projects. |
| Exposure data | Shodan/Censys: no meaningful fingerprintable footprint. Effectively zero enterprise install base. |
| Disclosure | 2026-06-28 via VulDB. Researcher credit per VulDB entry; coordinated only in the loosest sense. |
Sources.
- VulDB — CVE-2026-13485
- Vulnerability-Lookup CIRCL — CVE-2026-13485
- GHSA-Q3VP-2MQC-929R (CIRCL mirror)
- Sibling CVE-2026-13488 (preview7.php SQLi)
- Researcher PoC — lffaker/cybersec issue #6 (preview.php)
- Researcher PoC — lffaker/cybersec issue #4 (archive.php)
- Researcher PoC — yan-124/yan issue #4 (preview7.php)
- SourceCodester project home
Why this verdict
- Installed base near zero in enterprise: SourceCodester is a free code-sample repository, not an enterprise product. If you don't run it (and you almost certainly don't), this CVE is noise.
- Role multiplier — low-value role only: affected component is a standalone PHP class-scheduling demo. It is never deployed as IdP, hypervisor, CA, backup, EDR, or any high-value role. Blast radius caps at one app database on one host — there is no fleet, domain, or supply-chain pivot path. Floor stays at LOW.
- Friction — discovery is the bottleneck: there is no Shodan/Censys fingerprint for this product, no fleet scanning, and EPSS predicts <0.5% exploitation probability. Even motivated attackers ignore it because the loot tables are student schedules.
- No vendor patch pipeline: SourceCodester does not maintain CVE-keyed patches for community uploads, so the realistic remediation is *remove the app* or *replace with a maintained scheduler*, not 'apply update'.
Why not higher?
MEDIUM and above would imply this belongs in a normal patch queue with real ticket velocity. It does not — the product has no enterprise footprint, no patch supply chain, and the only realistic victim population (student-project deployers) is not the audience defending a 10,000-host fleet. Calling this HIGH inflates the queue and dilutes signal on bugs that matter.
Why not lower?
IGNORE would be wrong because the vulnerability IS trivially exploitable with public PoC, and a handful of organizations *might* genuinely run a SourceCodester app inside a lab or staging environment. If your asset inventory surfaces one, you need to act — just at a backlog-hygiene priority, not at incident pace.
Crowdsourced verification payload.
Run on any auditor workstation with network reach to the suspected host. Requires curl only; no privileges needed beyond outbound HTTP. Invoke as ./check_cve_2026_13485.sh https://target.example.com — script returns VULNERABLE if the app fingerprints as the SourceCodester scheduler and the vulnerable parameter responds with SQL-error-shaped output.
#!/usr/bin/env bash
# noisgate verifier — CVE-2026-13485
# SourceCodester Class and Exam Timetabling System 1.0 — SQLi in /preview.php
# Usage: ./check_cve_2026_13485.sh <base_url>
set -u
BASE="${1:-}"
if [[ -z "$BASE" ]]; then
echo "Usage: $0 <base_url> e.g. https://timetable.lab.local" >&2
exit 2
fi
BASE="${BASE%/}"
UA="noisgate-verifier/1.0"
TMP="$(mktemp)"
trap 'rm -f "$TMP"' EXIT
# Step 1: fingerprint the app at all
FP=$(curl -sk -A "$UA" --max-time 10 -o "$TMP" -w "%{http_code}" "$BASE/preview.php?course_year_section=1")
if [[ "$FP" == "000" || "$FP" == "404" ]]; then
echo "UNKNOWN — /preview.php not reachable (HTTP $FP). Asset may not run this product."
exit 0
fi
# Step 2: probe with a benign single-quote — a vulnerable build returns a MySQL error
PROBE=$(curl -sk -A "$UA" --max-time 10 "$BASE/preview.php?course_year_section=1%27")
if echo "$PROBE" | grep -qiE 'you have an error in your sql syntax|mysql_fetch|mysqli_|sqlstate|warning: mysql'; then
echo "VULNERABLE — /preview.php course_year_section emits SQL error on single quote."
exit 1
fi
# Step 3: boolean differential — same length response for 1=1 vs 1=2 means likely sanitized
TRUE_LEN=$(curl -sk -A "$UA" --max-time 10 "$BASE/preview.php?course_year_section=1%20AND%201%3D1" | wc -c)
FALSE_LEN=$(curl -sk -A "$UA" --max-time 10 "$BASE/preview.php?course_year_section=1%20AND%201%3D2" | wc -c)
DIFF=$(( TRUE_LEN > FALSE_LEN ? TRUE_LEN - FALSE_LEN : FALSE_LEN - TRUE_LEN ))
if (( DIFF > 50 )); then
echo "VULNERABLE — boolean-based differential detected (delta=$DIFF bytes)."
exit 1
fi
echo "PATCHED or NOT-AFFECTED — no SQL error, no boolean differential on /preview.php."
exit 0