munotes®

Security Validation

Get access to whole semester resourcesSemester Pass

Chapter Sixty-Five

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

Pages 432 to 436 of 499

In one line

Security validation is going down the security design line by line and showing, for each control, that the system really does what the design claims, by attacking it in the way that control exists to stop.

In the wording to use when asked: security validation verifies that the controls identified in the security design are implemented and effective, by exercising each against the threat it addresses, and includes reviewing dependencies for known vulnerabilities; its result is a report stating, for each control, the evidence and any residual risk accepted.

The rule for this chapter

A control is not tested by reading the code that implements it. It is tested by doing the thing it exists to prevent and finding that you cannot. So each row below is an attack, and the evidence is what came back.

Seventeen controls were designed (Chapter 31). Fourteen are shown here; the other three are shown by their own chapters and named at the end.

The automated ones

Five controls are in the test suite, named after themselves, so that the report can be produced by running it (Chapter 53):

$ cd ~/canteen-preorder
$ node --test --test-reporter=./test/reporter.js \
>   test/integration/security.test.js 2>&1 | tail -8
test/integration/security.test.js
  pass  S6 keeps only a hash of the session token
  pass  S8 treats SQL typed into a field as text
  pass  S10 refuses a request body over 10 kilobytes
  pass  S11 answers a broken body without the details
  pass  S12 marks the cookie Secure and sends HSTS under HTTPS

5 tests: 5 passed, 0 failed
  • S6, the session: the cookie's value is 32 random bytes, and what the database holds is its SHA-256, so a stolen sessions table signs nobody in.
  • S8, injection: a menu item named Tea'); DROP TABLE orders; -- is stored as text, and the orders table is still there.
  • S10, oversized input: a body over 10 kilobytes is refused 413 before any route sees it.
  • S11, error details: a broken body answers bad_json with no SyntaxError, no stack and no file name.
  • S12, the cookie under HTTPS: with COOKIE_SECURE=true the cookie is marked Secure and the site sends its HSTS header.

The ones that need an attack

S1: one student cannot touch another's order

$ cd ~/canteen-preorder
$ npm start > sec.log 2>&1 &
$ curl -s -o /dev/null --retry 10 --retry-connrefused localhost:3000/api/health
$ curl -s -c priya -o /dev/null -H 'Content-Type: application/json' \
>   -d '{"email":"priya@college.example","password":"canteen-demo"}' \
>   localhost:3000/api/auth/login
$ ID=$(curl -s -b priya -H 'Content-Type: application/json' \
>   -d '{"slot":"12:40","items":[{"menuItemId":1,"quantity":1}]}' \
>   localhost:3000/api/orders | jq -r .order.id); echo "Priya's order is $ID"
Priya's order is 1
$ curl -s -c kabir -o /dev/null -H 'Content-Type: application/json' \
>   -d '{"email":"kabir@college.example","password":"canteen-demo"}' \
>   localhost:3000/api/auth/login
$ curl -s -b kabir -o /dev/null -w "Kabir reads it:    %{http_code}\n" \
>   localhost:3000/api/orders/$ID
Kabir reads it:    404
$ curl -s -b kabir -o /dev/null -w "Kabir cancels it:  %{http_code}\n" \
>   -X POST -H 'Content-Type: application/json' -d '{}' \
>   localhost:3000/api/orders/$ID/cancel
Kabir cancels it:  404
$ curl -s -b kabir localhost:3000/api/orders/mine | jq -c '.orders | length'
0
munotes.in432

Security Validation

404 both times, which is the design: not "forbidden", which would confirm that the order exists, but "not found" (Chapter 43). And Kabir's own list is empty, so nothing leaked into it.

S2: a student cannot use the counter's or the owner's functions

$ cd ~/canteen-preorder
$ for p in /api/orders /api/kitchen?slot=12:40 /api/reports/daily; do \
>   curl -s -b priya -o /dev/null -w "%{http_code} $p\n" "localhost:3000$p"; done
403 /api/orders
403 /api/kitchen?slot=12:40
403 /api/reports/daily
$ curl -s -b priya -o /dev/null -w "%{http_code} POST /api/menu\n" \
>   -H 'Content-Type: application/json' \
>   -d '{"name":"Free Lunch","category":"meals","pricePaise":100,"isVeg":true}' \
>   localhost:3000/api/menu
403 POST /api/menu
$ curl -s -b priya -o /dev/null -w "%{http_code} POST /api/users/staff\n" \
>   -H 'Content-Type: application/json' \
>   -d '{"name":"Me Again","email":"me@college.example","password":"a-good-one-42"}' \
>   localhost:3000/api/users/staff
403 POST /api/users/staff

Five addresses, five 403s: deny by default, enforced by the guard in front of each route (Chapter 42).

S4 and S16: guessing, and finding out who has an account

The limiter is tested in the suite and again in Chapter 60, where its dependence on TRUST_PROXY is shown. S16, that a wrong password and an unknown email are answered alike, in time as well as in words, is measured in Chapter 46: 156 ms against 151 ms, because the service hashes a dummy password when the email is unknown.

S7: another site cannot act for a signed-in student

$ cd ~/canteen-preorder
$ curl -s -b priya -o /dev/null -w "as a form post:    %{http_code}\n" \
>   -H 'Content-Type: application/x-www-form-urlencoded' \
>   -d 'slot=12:40' localhost:3000/api/orders
as a form post:    415
$ curl -s -b priya -o /dev/null -w "from another site: %{http_code}\n" \
>   -H 'Content-Type: application/json' -H 'Origin: https://evil.example' \
>   -d '{"slot":"12:40","items":[]}' localhost:3000/api/orders
from another site: 403

An ordinary form on another website can send neither: it cannot set the content type to JSON, and its origin is not this site. Together with the cookie's SameSite=Lax that is three defences against cross-site request forgery, and the first two are shown here.

S9 and S17: the browser's own protections

$ cd ~/canteen-preorder
$ curl -s -D - -o /dev/null localhost:3000/api/menu | \
>   grep -iE 'content-security-policy|x-frame-options|x-content-type|referrer|x-powered-by'
Content-Security-Policy: default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
X-Frame-Options: DENY

The content security policy allows scripts only from the site itself, so a script injected into a page would not run even if one ever were; the framing headers stop the site being shown inside another site's page; and x-powered-by is absent, because the application removes it (Chapter 42).

munotes.in433

Security Validation

S3: passwords

Shown in Chapter 46, run: the stored form is scrypt$16384$8$5$salt$key, two hashes of one password differ, and one hash costs about 160 milliseconds on purpose. Here is the part that matters to an attacker who has the database:

$ cd ~/canteen-preorder
$ sudo mysql canteen -e \
>   "SELECT email, LEFT(password_hash, 22) AS beginning FROM users LIMIT 3;"
email	beginning
owner@college.example	scrypt$16384$8$5$ksghO
counter@college.example	scrypt$16384$8$5$SSxy/
priya@college.example	scrypt$16384$8$5$KS08D

Nothing in that column can be turned back into a password, and the settings travel with each hash so they can be raised later.

S15: the dependencies

$ cd ~/canteen-preorder
$ npm audit 2>&1 | tail -3
found 0 vulnerabilities
$ node -e "
> const p = require('./package.json');
> console.log('direct dependencies:', JSON.stringify(p.dependencies));
> const n = require('node:fs').readdirSync('node_modules')
>   .filter((d) => !d.startsWith('.')).length;
> console.log('packages installed in all:', n);
> "
direct dependencies: {"express":"5.2.1","mysql2":"3.24.5"}
packages installed in all: 75

Two direct dependencies, at exact versions, installed from a lock file (Chapter 39), and no known vulnerability in any of the seventy-five packages in node_modules: the two, and everything they bring with them. Run this before every release, because the answer changes without your code changing: a vulnerability published tomorrow is in your project tomorrow.

S13: secrets

$ cd ~/canteen-preorder
$ grep -rn "DB_PASSWORD" .env.example src/config.js | head -3
.env.example:14:DB_PASSWORD=change-this-password
src/config.js:18:      password: env.DB_PASSWORD || '',
src/config.js:30:    throw new Error('DB_PASSWORD is not set. '
$ grep -c "^.env$" .gitignore
1
$ ls -l .env | awk '{print $1, $NF}'
-rw-r--r-- .env
$ chmod 600 .env
$ ls -l .env | awk '{print $1, $NF}'
-rw------- .env

The password is read from the environment, the example file carries a placeholder, and .env is ignored by Git (Chapter 61 shows Git really ignoring it). The last three lines are the part students miss: the file was readable by everyone on the machine, and chmod 600 makes it readable only by its owner. On the lab desktop that owner is the service's own user (Chapter 60); here it is the student.

The two this lab cannot show

  • S12's HTTPS in use needs a certificate and a public name, which the trial does not have (Chapter 25). What is shown here is the application's half: with COOKIE_SECURE=true the cookie is marked Secure and the HSTS header is sent.
  • S14, the database and the application reachable only from the machine, is a property of the deployment rather than of the code: Chapter 57 shows the application refusing the network address until it is told otherwise, and Chapter 60 configures Nginx as the only program facing it.
munotes.in434

Security Validation

The security test report

One page for the final report (Chapter 73), and the honest parts are the last two rows:

Security validation, Canteen Pre-order, 14 October 2026

Method. Each control of the security design was exercised against the threat it addresses, on the deployed system, plus the automated security tests and a dependency audit.

Result. S1 to S11 and S13 to S17: no control failed. S12 is met in the application, and not in the trial's deployment, which has no HTTPS.

Dependencies. npm audit: no known vulnerabilities in the two direct dependencies or the packages they bring.

Accepted risks. Plain HTTP on the college Wi-Fi during the trial (the largest, with its fix in Chapter 58); an 8-character minimum password against NIST's 15; no alerting on bursts of failed sign-ins; one database account with every privilege on its own two databases; and registering with an address that already has an account reveals that it is registered.

Not tested. Anything below the application: the operating system's own patches, the college network, and MySQL itself.

A security report with no accepted risks and nothing untested is not a thorough report; it is an incomplete one.

Do this for your project

  1. Write the security design first, with numbered controls (Chapter 31), and test against that list.
  2. Test each control by doing the thing it prevents, not by reading its code.
  3. Put the controls a program can check into the test suite, named after themselves.
  4. Run a dependency audit before every release, and again before the examination.
  5. Say plainly which controls your environment cannot demonstrate, and why.
  6. List the risks you accept, each with its reason.
  7. Say what you did not test at all.

Mistakes that cost marks

"The system is secure", with nothing tested.

Testing a control by reading its code, which proves only that the code exists.

A password hash pasted into a report as evidence, which is a leak, not evidence.

A dependency audit run once, in August.

No accepted risks, which means either a perfect system or an unread design.

Attacking somebody else's system to show a technique: everything in this chapter is done to the team's own application, on their own machine, and nothing else is acceptable.

Quick revision

  • Test a control by doing what it prevents; reading the code is not a test.
  • In the suite: S6 hashed sessions, S8 injection, S10 size, S11 error details, S12 the Secure cookie.
  • By attack: S1 another's order is 404, S2 five addresses 403, S7 form post and foreign origin refused, S9 and S17 the headers, S3 the stored hash, S13 the secret's place.
  • npm audit before every release: the answer changes without your code changing.
  • Say what the environment cannot show, the risks accepted, and what was not tested.
munotes.in435

Security Validation

Questions you must be able to answer

1. What does security validation add to the tests already written? It goes down the security design's list of controls and exercises each against the threat it exists to stop, on the deployed system, rather than testing that the application's features work. Its output is a statement about each control, with evidence.

2. Why is reading the code not a test of a control? Because the code may be right and unreachable, applied to the wrong route, or undone by something else. The test is to attempt the thing the control prevents and find that it fails.

3. Why does a student asking for another student's order get 404 rather than 403? Because 403 would confirm that the order exists and belongs to somebody, which is information the attacker did not have. "Not found" is the same answer an id that names nothing receives.

4. What are the three defences against another site acting for a signed-in student? The request must be JSON, which an ordinary cross-site form cannot send; if the browser states the origin, it must be this site; and the session cookie is SameSite=Lax, so it is not sent with another site's form posts.

5. Why must a dependency audit be run again before the examination? Because it reports vulnerabilities published since the last run: the project's own code need not change for the answer to change.

6. What makes a security report credible? That it says which controls were tested and how, which the environment could not demonstrate, which risks were accepted and why, and what was not tested at all. A report with none of those reads as a claim rather than a result.

munotes.in436

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!