munotes®

Input Validation Checks

Get access to whole semester resourcesSemester Pass

Chapter Sixty-Four

Syllabus topic Module 2, "Performance & Security Testing: Input validation checks".

Pages 426 to 431 of 499

In one line

An input validation check is testing what the system does with everything a user could send, not what it does when used properly: wrong types, missing fields, empty values, enormous values, values one past a limit, and values that look like attacks, at every address that takes input, with nothing answering with a server error.

In the wording to use when asked: input validation testing is a systematic exercise of every input to a system with values outside its expected domain, verifying that each is rejected at the correct boundary with a defined error response, that no input causes an unhandled failure, and that inputs are neither silently coerced nor stored in a form that changes their meaning.

Why it is a security check and not only a testing one

MU lists it under Performance & Security Testing, beside load testing and security validation, and that is the right place. Unvalidated input is where most attacks on web applications begin (Chapter 31): injection, oversized bodies that exhaust memory, values that reach the database and break it. The tests of Chapter 52 ask whether the rules are right; this check asks whether anything at all can get past them.

The difference in practice is the attitude. A black-box test asks "does 6 get refused?" An input validation check asks "what happens if I send an array where a number goes, a string of 200 characters, a number larger than the column, a null, nothing at all, or ' OR 1=1 --?"

What to send

A checklist that covers most of what finds faults, for every field of every request:

KindExamples
The wrong type"3" for a number, 3 for a string, an array, an object, true
Missing and emptyabsent, null, "", [], {}
Boundariesone below, the edge, one above, for every limit
Enormous200 characters where 60 are allowed, 1e308, a number above the column's range
Strange charactersquotes, angle brackets, a null character, characters from another script
Attack shapes' OR 1=1 --, <script>alert(1)</script>, ../../etc/passwd
The request itselfnot JSON, broken JSON, a body over the limit, a missing content type
The addressan id that is a word, zero, negative, enormous, or has a leading zero

And every answer is judged against three rules:

  1. Nothing may answer 500. A server error means the application met something it did not expect.
  2. Every refusal names the field, so a page can show it where it belongs.
  3. Nothing is quietly converted. "3" must be refused, not read as 3, because a page that sends it has a bug worth finding (Chapter 47).

The sweep

// Input validation checks: every field of every endpoint that
// takes input, with values from the checklist. Nothing should
// answer 500, and every refusal should name its field.
const BASE = 'http://127.0.0.1:3000';
const jar = {};

async function send(who, method, path, body, raw) {
  const headers = {};
  if (jar[who]) headers.Cookie = jar[who];
  let payload;
  if (raw) {
    headers['Content-Type'] = raw.type;
    payload = raw.body;
  } else if (method !== 'GET') {
    headers['Content-Type'] = 'application/json';
    payload = JSON.stringify(body ?? {});
  }
  const res = await fetch(BASE + path, { method, headers, body: payload });
  const set = res.headers.get('set-cookie');
  if (set) jar[who] = set.split(';')[0];
  const data = await res.json().catch(() => null);
  return {
    status: res.status,
    code: data?.error?.code ?? '',
    fields: Object.keys(data?.error?.details ?? {}).join(','),
  };
}

const WRONG = [null, true, 0, -1, 1.5, '', ' ', 'abc', [], {},
  'x'.repeat(200), "' OR 1=1 --", '<script>alert(1)</script>',
  '../../etc/passwd', '\u0000', 1e308, 4294967296];

const results = [];
// `value` is what was sent, shortened, so that an accepted
// probe can be judged without running the script again.
const short = (v) => {
  const s = JSON.stringify(v) ?? String(v);
  return s.length <= 20 ? s : `${s.slice(0, 17)}..."`;
};
async function check(area, value, ...args) {
  const r = await send(...args);
  results.push({ area, value: short(value), ...r });
  return r;
}

(async () => {
  // Every field of registering, which is open to anyone.
  for (const wrong of WRONG) {
    await check('register name', wrong, 'anon', 'POST', '/api/auth/register',
      { name: wrong, email: 'a@college.example', password: 'a-good-one-42' });
    await check('register email', wrong, 'anon', 'POST', '/api/auth/register',
      { name: 'Test Student', email: wrong, password: 'a-good-one-42' });
    await check('register password', wrong, 'anon', 'POST', '/api/auth/register',
      { name: 'Test Student', email: 'b@college.example', password: wrong });
    await check('login email', wrong, 'anon', 'POST', '/api/auth/login',
      { email: wrong, password: 'x' });
  }
  await send('priya', 'POST', '/api/auth/login',
    { email: 'priya@college.example', password: 'canteen-demo' });
  await send('lata', 'POST', '/api/auth/login',
    { email: 'owner@college.example', password: 'canteen-demo' });
  // Every field of an order, and of the owner's screens.
  for (const wrong of WRONG) {
    await check('order slot', wrong, 'priya', 'POST', '/api/orders',
      { slot: wrong, items: [{ menuItemId: 1, quantity: 1 }] });
    await check('order items', wrong, 'priya', 'POST', '/api/orders',
      { slot: '12:40', items: wrong });
    await check('order itemId', wrong, 'priya', 'POST', '/api/orders',
      { slot: '12:40', items: [{ menuItemId: wrong, quantity: 1 }] });
    await check('order quantity', wrong, 'priya', 'POST', '/api/orders',
      { slot: '12:40', items: [{ menuItemId: 1, quantity: wrong }] });
    await check('menu name', wrong, 'lata', 'POST', '/api/menu',
      { name: wrong, category: 'snacks', pricePaise: 3000, isVeg: true });
    await check('menu price', wrong, 'lata', 'POST', '/api/menu',
      { name: 'Test Item', category: 'snacks', pricePaise: wrong, isVeg: true });
    await check('stock', wrong, 'lata', 'PUT', '/api/menu/1/stock',
      { stockLeft: wrong });
    await check('status', wrong, 'lata', 'PATCH', '/api/orders/1/status',
      { status: wrong });
  }
  // Ids in the address, and queries.
  for (const id of ['abc', '0', '-1', '1.5', '017', '99999999999',
    '1e3', '%20', '..']) {
    await check('order id', id, 'priya', 'GET', `/api/orders/${id}`);
    await check('menu id', id, 'lata', 'PUT', `/api/menu/${id}/stock`,
      { stockLeft: 5 });
  }
  for (const q of ['slot=bad', 'slot=', 'slot=12:40&slot=12:50',
    'slot[]=12:40', 'date=2026-02-30', 'date=abc', 'date[]=1']) {
    await check('query orders', q, 'lata', 'GET', `/api/orders?${q}`);
    await check('query report', q, 'lata', 'GET', `/api/reports/daily?${q}`);
  }
  // The request itself.
  await check('body', 'not JSON', 'priya', 'POST', '/api/orders', null,
    { type: 'text/plain', body: 'slot=12:40' });
  await check('body', 'broken JSON', 'priya', 'POST', '/api/orders', null,
    { type: 'application/json', body: '{"slot":' });
  await check('body', '11 kB', 'priya', 'POST', '/api/orders', null,
    { type: 'application/json', body: JSON.stringify({ p: 'x'.repeat(11000) }) });

  // The report: how each area answered, and the three rules.
  const areas = [...new Set(results.map((r) => r.area))];
  for (const area of areas) {
    const mine = results.filter((r) => r.area === area);
    const answers = [...new Set(mine.map((r) => `${r.status} ${r.code}`))];
    console.log(`${area.padEnd(17)} ${String(mine.length).padStart(3)} `
      + `probes  ${answers.sort().join(' | ')}`);
  }
  const server = results.filter((r) => r.status >= 500);
  const accepted = results.filter((r) => r.status < 400);
  const unnamed = results.filter(
    (r) => r.code === 'invalid_input' && r.fields === '');
  console.log(`\nprobes ${results.length}: server errors ${server.length}, `
    + `accepted ${accepted.length}, refusals naming no field ${unnamed.length}`);
  for (const a of accepted) {
    console.log(`  accepted: ${a.area.padEnd(17)} ${a.value}`);
  }
})();
munotes.in426

Input Validation Checks

$ cd ~/canteen-preorder
$ npm run db:setup > /dev/null
$ npm start > checks.log 2>&1 &
$ curl -s -o /dev/null --retry 10 --retry-connrefused localhost:3000/api/health
$ node ~/checks.js
register name      17 probes  201  | 400 invalid_input
register email     17 probes  400 invalid_input
register password  17 probes  201  | 400 invalid_input | 409 email_taken
login email        17 probes  400 invalid_input | 401 wrong_credentials
order slot         17 probes  400 invalid_input
order items        17 probes  400 invalid_input
order itemId       17 probes  400 invalid_input
order quantity     17 probes  400 invalid_input
menu name          17 probes  201  | 400 invalid_input
menu price         17 probes  400 invalid_input
stock              17 probes  200  | 400 invalid_input
status             17 probes  400 invalid_input
order id            9 probes  404 not_found
menu id             9 probes  404 not_found
query orders        7 probes  200  | 400 invalid_input
query report        7 probes  200  | 400 invalid_input
body                3 probes  400 bad_json | 413 too_large | 415 json_only

probes 239: server errors 0, accepted 14, refusals naming no field 0
  accepted: register name     "abc"
  accepted: register password "' OR 1=1 --"
  accepted: stock             0
  accepted: menu name         "abc"
  accepted: menu name         "' OR 1=1 --"
  accepted: menu name         "<script>alert(1)..."
  accepted: menu name         "../../etc/passwd"
  accepted: query report      "slot=bad"
  accepted: query report      "slot="
  accepted: query report      "slot=12:40&slot=..."
  accepted: query orders      "slot[]=12:40"
  accepted: query report      "slot[]=12:40"
  accepted: query orders      "date[]=1"
  accepted: query report      "date[]=1"
$ rm ~/checks.js
munotes.in427

Input Validation Checks

Reading the report

No server error anywhere, which is the first rule and the one that matters most: 239 probes, and nothing in the checklist reached code that did not expect it.

munotes.in428

Input Validation Checks

Every refusal named its field, which is the second rule: a page can put each message where it belongs.

Every area answers in one or two ways, and each is the right one: 400 invalid_input for a value that is wrong whatever the state of the canteen, 404 for an id that cannot name a row (Chapter 47), 409 where the request is proper but the canteen's state refuses it, and 415, 400 or 413 for the request itself. Notice login email: a wrong-typed email is a 400, but a well-formed one that nobody has is 401, the same answer as a wrong password, which is control S16 (Chapter 46).

The fourteen accepted probes are the part to read, and each is correct:

AcceptedWhy it is right
register name and menu name: "abc"three letters is a name; the rule is 2 to 80, and 2 to 60 for an item
register password: "' OR 1=1 --"eleven characters, not on the common list: a fine password. No composition rules (Chapter 46)
menu name: "' OR 1=1 --", "<script>alert(1)</script>", "../../etc/passwd"they are stored as text and shown as text: the SQL is a placeholder (Chapter 44) and the page adds text, never HTML (Chapter 41)
stock: 0nothing left today is a real answer, and the rule is 0 to 1000
query report: slot=bad, slot=, a repeated slotthe daily report has no slot parameter at all, so an unknown one is ignored, as every unknown parameter is
query orders and query report: slot[]=12:40, date[]=1the names are slot[] and date[], which the application has no parameter called, so no filter is applied

The third of those rows is worth pausing on. A menu item may be named <script>alert(1)</script>, and that is the correct behaviour: the name is data, and the two places it could become code are both closed. Refusing angle brackets instead would be the blacklist Chapter 47 argues against, and it would refuse real names in other languages long before it stopped an attacker.

What the check made the team look at

One thing, and it became a decision rather than a defect. A menu item's name keeps the spacing inside it: Veg Thali is stored as typed, because the validator trims the ends and checks the length but does not collapse runs of spaces. Nothing breaks, HTML shows it as one space, and the database's unique index treats Veg Thali and Veg Thali as different names.

munotes.in429

Input Validation Checks

The worked team looked at it and left it, recording why: the owner types the names herself and sees them on her own screen at once, and collapsing spaces silently would be one more quiet change to what somebody typed. Writing that down is the point. An accepted input nobody has thought about is a hole; an accepted input with a recorded reason is a decision.

What the check does not cover

Being honest about the edges of a check is part of it:

  • It sends one value at a time. Combinations, such as a valid slot with an invalid item, are the decision table's job (Chapter 52).
  • It does not test order: two requests that race are Chapter 45's.
  • It cannot see what the page does with a refusal; that is Chapter 41, and it is where the defect of Chapter 56 lived.
  • It tests the API, not the database's own constraints, which are the last net (Chapter 29).

Do this for your project

  1. List every endpoint that takes input, and every field of each.
  2. Send the checklist's kinds to each: wrong type, missing, empty, boundaries, enormous, strange characters, attack shapes.
  3. Probe the ids in addresses and the values in queries as well as the bodies.
  4. Test the request itself: not JSON, broken JSON, too large.
  5. Judge by three rules: nothing 500, every refusal names its field, nothing quietly converted.
  6. Read every accepted probe, and either fix it or write down why it is right.
  7. Keep the script in the repository and put its report in the test summary.

Mistakes that cost marks

Checking only the happy path, which is the one nobody gets wrong.

A 500 dismissed as "it only happens with silly input", which is precisely the input an attacker sends.

Silent conversion, so "3" becomes 3 and the page's bug is never found.

Refusals with no field, which a page cannot show where it belongs.

Accepted probes unread, so the one that should have been refused is never noticed.

A check run once by hand and never again after the code changed.

Quick revision

  • The checklist: wrong type, missing and empty, boundaries, enormous, strange characters, attack shapes, the request itself, the address.
  • Three rules: nothing 500, every refusal names its field, nothing quietly converted.
  • Read the accepted probes: fix, or record the reason.
  • 400 for always wrong, 404 for an id that names nothing, 409 for wrong now, 413 and 415 for the request itself.
  • It does not cover combinations, races, what the page shows, or the database's own constraints.
munotes.in430

Input Validation Checks

Questions you must be able to answer

1. Why is input validation checking listed under security testing? Because unvalidated input is where most attacks on web applications begin: injection, oversized requests, and values that reach the database in a form nobody expected. The check asks whether anything can get past the validation at all, which is a security question.

2. How does this check differ from black-box testing of the same rules? Black-box tests ask whether the specified rules are applied correctly, with values chosen from partitions and boundaries. This check sends values outside the expected domain entirely, including wrong types and attack shapes, at every field of every endpoint, and judges the answers by rules that hold everywhere.

3. What are the three rules every answer is judged by? Nothing may answer with a server error, because that means the application met something it did not expect; every refusal of input must name the field at fault, so the page can show it there; and nothing may be quietly converted, because a value of the wrong type means the caller has a bug.

4. Why is the list of accepted probes the most interesting part of the report? Because a refusal is the expected outcome, while every acceptance is a claim that the value is valid. Reading them finds the one that should have been refused, and turns the rest into recorded decisions.

5. What did this check find in the worked project? That a menu item's name keeps the spacing inside it, so two names differing only in spaces are different names. The team left it, recording why: the owner types and sees the names herself, and silently changing what somebody typed is worse than storing it.

6. What does the check not cover? Combinations of conditions, which a decision table covers; requests that race each other, which concurrency tests cover; what the pages do with a refusal, which is seen only in a browser; and the database's own constraints.

munotes.in431

The rest of this subject

These notes are cut from the University's printed syllabus. Open the syllabus itself for the same subject.

Issue
Done!