NEB 12 Computer Science study materials
NEB 12 Computer Science study resources

The Admission Form That Trusted the Browser

NEB 12 Computer Science · Put your knowledge to work in context.

All case studies

The Admission Form That Trusted the Browser

Sagarmatha Secondary School publishes an online admission form for Grade 11: apply.php receives roll number, full name and marks obtained, then inserts them into a MySQL table using PHP. About 1,200 applications are expected. The current code builds the statement by string concatenation, $sql = "INSERT INTO applicants (roll, name, marks) VALUES ('$roll', '$name', '$marks')";, and the marks column was created as VARCHAR(10) because 'it holds a number, so text is fine'. Three things went wrong in the trial run. An applicant named O'Brien could not be saved at all. During testing, someone typed ' OR '1'='1 into the roll box and the page displayed every applicant in the table. And the school's own merit list looked scrambled: 100, 99, 9, 87, 85, 78 - a student with 9 marks appeared above one with 87. The form does have JavaScript that checks the marks field before submission, so the team believes validation is already handled.

1. Explain precisely why O'Brien's record failed and why the injected roll number returned the whole table. Rewrite the insert using a prepared statement and say what the prepared statement changes about how MySQL parses the input.

Read the worked answer

O'Brien. In the concatenated string the apostrophe in O'Brien closes the SQL string literal early, so MySQL receives ... VALUES ('12', 'O'Brien', '85') and reports a syntax error near 'Brien'. The query never runs. The PHP side is no better: because the literal uses double quotes, PHP also interpolates $name directly, so there is no layer at all between user text and SQL grammar.

The injection. ' OR '1'='1 in the roll box turns the WHERE clause (or the VALUES list) into a tautology, so the SELECT behind the results page returns every row. The same trick with '; DROP TABLE applicants; -- would be destructive. Note this only 'worked' because the tester typed it - an attacker does not need the form, only an HTTP POST, which is why the JavaScript check is irrelevant here.

Fix:

$stmt = $pdo->prepare('INSERT INTO applicants (roll, name, marks) VALUES (?, ?, ?)');
$stmt->execute([$roll, $name, $marks]);

With a prepared statement MySQL compiles the statement structure first - the placeholders are fixed - and then binds the values separately. The value O'Brien is never re-parsed as SQL, so the apostrophe is just a character; the same binding makes ' OR '1'='1 an ordinary string that matches no roll number. Use the same pattern for the merit-list SELECT, and never interpolate a value into a string, even for numbers.

2. The team argues that the JavaScript validation already protects the marks field. Assess that claim and write the server-side check for marks between 0 and 100, including what PHP should do when the check fails.

Read the worked answer

Claim is wrong. JavaScript runs in the applicant's browser, on a page the applicant controls. It is skipped by disabling JavaScript, by saving the page and editing it, or by sending a POST with curl or Postman - none of which ever load the script. Its only real value is convenience: fast feedback before the round trip. Anything that affects the database must be re-checked on the server, because the server is the only place the school controls.

Server-side check:

$raw   = $_POST['marks'] ?? '';
$marks = filter_input(INPUT_POST, 'marks', FILTER_VALIDATE_INT,
                      ['options' => ['min_range' => 0, 'max_range' => 100]]);
if ($marks === false) {
    $errors[] = 'Marks must be a whole number between 0 and 100.';
}

Reasoning behind each choice: filter_input reads the POST source directly and returns false for '85abc', ' 85 ' with letters, an empty string, or 101, so one call covers type and range; the strict === false test is essential because a valid 0 is falsy and if (!$marks) would wrongly reject it; the raw value is kept only so the form can be re-displayed. On failure the script must not execute the INSERT - it should re-render the form with the previous values escaped through htmlspecialchars() and the error messages shown to the applicant. Validation and output escaping are separate jobs: validation keeps bad data out, escaping keeps stored data from breaking the page.

3. Explain why the merit list produced 100, 99, 9, 87, 85, 78, and give two ways to fix it. Which fix is correct for the long term, and why?

Read the worked answer

The column is VARCHAR(10), so MySQL sorts it as text, comparing character by character. '100' and '9' both start with different characters; '1' has a lower code than '9', so '100' sorts before '9'. Likewise '9' sorts after '87' because '9' > '8'. The list is alphabetically ordered, not numerically ordered - the numbers are being stored as words.

Quick fix (query level):

SELECT roll, name, marks FROM applicants ORDER BY CAST(marks AS UNSIGNED) DESC;

This works for display, but the CAST runs on every row, so an index on marks cannot be used, and any other query (averages, filters marks > 80, comparisons with pass marks) has to remember the cast. It also lets '85 ' and '85' both exist, and lets '82abc' exist at all.

Correct fix (schema level):

ALTER TABLE applicants MODIFY marks TINYINT UNSIGNED NOT NULL;

TINYINT UNSIGNED holds 0–255, exactly the required range, in one byte, sorts numerically and indexes properly, and rejects non-numeric text at the database boundary - a second line of defence behind the PHP filter. The general principle for the exam: choose the column type from the meaning of the data, not from what happens to type easily into the form. Zero-padding to three digits ('085') would also make text sorting look correct, but it is cosmetic and breaks the moment a value exceeds three digits.