Search Tech Journey

Find topics, journeys and posts

back to blog
systemsintermediate 32m read

R17 · Week 17 Recall & Drill

Week 17 revision: authorisation per resource not per login, TLS 1.3 and the chain of trust, injection as a parsing problem, the ML lifecycle scaffold, and least squares by hand.

🔒SecurityRevision · Week 17· Session 017 of 130 90 min

🎯 Rebuild Week 17 from a blank page: authentication happens once and authorisation happens per request, a certificate proves hostname control and nothing else, injection is data being parsed as code, every dataset is a matrix and a vector, and linear means linear in the parameters.

Weekly revision · Week 17 · Covers 5 sessions from Mon–Fri.

Sessions covered

By the end of this revision you can
  • Say how often authentication and authorisation each occur in a request lifecycle, and why the second must be per resource.
  • Explain why a general-purpose fast hash is the wrong tool for passwords, and what a purpose-built one gives you.
  • Name the three properties transport security provides and the ones it does not, and diagnose the four common certificate errors.
  • Explain injection as data reaching an interpreter, and give the correct fix rather than the sanitising one.
  • Describe any dataset as a feature matrix and label vector, and explain why validation and test sets are separate.
  • Write the least-squares closed form and its gradient, and recognise the three learning-rate failure modes.

90-min structure

BlockMinutesWhat you do
Warm-up recall5Five sessions, one sentence each.
Blank-page reconstruction30The per-session prompts below.
Hands-on drill30Hash timing, a certificate chain, an injection, and a leaky split.
Quiz + misconception15Answer before revealing.
Gap analysis + preview10Write the gaps. Skim next week.

Blank-page reconstruction · 30 min

S081 · AuthN vs AuthZ

  1. Define both terms and say which happens first and how often each happens.
  2. Explain why every password must be salted, and why a fast general-purpose hash is wrong.
  3. Give the practical trade between server-side sessions and self-contained tokens.

Gotcha you probably forgot: revoking a self-contained token before its expiry requires giving up the property that made it attractive. Since the server holds no record of it, you must add one — a revocation list or a short expiry with a refresh mechanism — at which point you are doing server-side lookups again. Decide up front whether you need revocation, because retrofitting it undoes the design.

S082 · TLS & PKI

  1. Name the three properties transport security provides, and one it does not.
  2. Explain the chain from leaf to intermediate to root, and what your client actually trusts.
  3. Diagnose each of the four common certificate errors in one line.

Gotcha you probably forgot: modern clients validate the hostname against the subject alternative name extension and ignore the common name field entirely. A certificate whose common name looks correct but whose alternative names omit the host will be rejected, and the error message points at the hostname rather than at the missing extension — which is why this one wastes so much time.

S083 · OWASP & Threat Modelling

  1. Name the category that sits at the top of the current list and say why it rose.
  2. Give the one-line fix for injection, and explain why sanitising is the wrong primitive.
  3. Name the six threat categories in the lightweight modelling framework.

Gotcha you probably forgot: server-side request forgery is dangerous specifically because internal endpoints reachable from inside your network often require no authentication at all — including cloud instance metadata services that hand out credentials to anything that asks. The mitigation is not a URL denylist, which is trivially bypassed; it is an allowlist of permitted destinations plus requiring authentication on the metadata path.

S084 · ML Mental Model

  1. Describe any dataset as a matrix and a vector, and state the shapes.
  2. Explain why the validation and test sets are separate things.
  3. Give the difference between the quantity being minimised and the quantity humans grade.

Gotcha you probably forgot: with a heavily imbalanced dataset, accuracy is actively misleading — a model that always predicts the majority class scores extremely well while being useless. Use precision and recall, or the area under the precision-recall curve, and always state the base rate alongside any accuracy figure so the number can be interpreted.

S085 · Linear Regression

  1. Write the closed-form solution in one line.
  2. Derive the gradient of the squared-error loss.
  3. Say when you would prefer iterative optimisation over the closed form.

Gotcha you probably forgot: the closed form fails when the feature matrix is rank-deficient — perfectly collinear features, or more features than examples — because the matrix being inverted is singular. The practical fixes are dropping the redundant feature, using a pseudo-inverse, or adding a regularisation term, which makes the matrix invertible by construction and is one reason regularisation is a default rather than an extra.


Hands-on drill · 30 min

Task: measure why password hashes are deliberately slow, inspect a real certificate chain, watch an injection succeed, and produce a leaked validation score.

mkdir -p ~/projects/w17-drill && cd ~/projects/w17-drill

Step 1 — slow on purpose (7 min)

# hashing.py
import hashlib
import os
import time
 
password = b"correct horse battery staple"
salt = os.urandom(16)
 
t0 = time.perf_counter()
for _ in range(100_000):
    hashlib.sha256(password).hexdigest()
fast = time.perf_counter() - t0
print(f"100k plain sha256 hashes: {fast:.3f}s  -> {100_000/fast:,.0f} hashes/sec")
 
for iterations in (10_000, 100_000, 600_000):
    t0 = time.perf_counter()
    hashlib.pbkdf2_hmac("sha256", password, salt, iterations)
    dt = time.perf_counter() - t0
    print(f"one derivation at {iterations:>7,} iterations: {dt*1000:7.1f} ms "
          f"-> attacker rate ~{1/dt:,.0f} guesses/sec/core")

Expected outcome: the plain hash runs at an enormous rate, which is exactly what makes it wrong here — an attacker with your database can test guesses at that rate. The derivation function takes a measurable fraction of a second for a single attempt, which is invisible to a user logging in once and reduces the attacker's throughput by orders of magnitude. The work factor is a dial you tune so a single verification costs a fraction of a second on your hardware; note also that the salt makes precomputed tables useless and forces the attacker to redo the work per account.

Step 2 — read a certificate chain (8 min)

# Fetch the chain and inspect what the client actually validates.
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName
# Prove the hostname check is done against the alternative names, not the subject.
echo "--- what the client checks ---"
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -ext subjectAltName
 
echo "--- deliberately wrong hostname: watch the failure mode ---"
curl -sS -o /dev/null -w '%{http_code}\n' https://example.com --resolve 'wrong.example:443:93.184.215.14' \
  https://wrong.example/ 2>&1 | tail -2
 
echo "--- days until expiry ---"
END=$(openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
      | openssl x509 -noout -enddate | cut -d= -f2)
python3 - "$END" <<'PY'
import sys, datetime
end = datetime.datetime.strptime(sys.argv[1].strip(), "%b %d %H:%M:%S %Y %Z")
print(f"expires {end:%Y-%m-%d}, in {(end - datetime.datetime.utcnow()).days} days")
PY

Expected outcome: you see the subject, the issuing intermediate, the validity window, and the alternative-name list. The mismatched hostname attempt fails at verification rather than at connection, which localises the error correctly. The expiry countdown is the one to internalise: certificate expiry is a scheduled outage that you are told about in advance and that still takes services down, which is why the only reliable answer is automated renewal plus an alert well before the date, not a calendar reminder.

Step 3 — injection is a parsing problem (8 min)

# injection.py
import sqlite3
 
con = sqlite3.connect(":memory:")
con.executescript("""
CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT, is_admin INTEGER DEFAULT 0);
INSERT INTO users(name, is_admin) VALUES ('ada', 0), ('root', 1);
""")
 
def vulnerable(name):
    return con.execute(f"SELECT id, name, is_admin FROM users WHERE name = '{name}'").fetchall()
 
def sanitising(name):
    cleaned = name.replace("'", "").replace(";", "").replace("--", "")   # the wrong primitive
    return con.execute(f"SELECT id, name, is_admin FROM users WHERE name = '{cleaned}'").fetchall()
 
def parameterised(name):
    return con.execute("SELECT id, name, is_admin FROM users WHERE name = ?", (name,)).fetchall()
 
payload = "ada' OR '1'='1"
print("vulnerable  :", vulnerable(payload))
print("sanitising  :", sanitising(payload))
print("parameterised:", parameterised(payload))
 
# The sanitiser was defeated without a single quote character.
tricky = "ada\" OR 1=1 OR \"x\"=\"x"
print("\nsanitising against a payload with no single quotes:", sanitising(tricky))

Expected outcome: the vulnerable query returns every row instead of one, because the input changed the structure of the statement rather than its data. The parameterised version returns nothing, since it looks for a user whose name literally contains that text — the input never reaches the parser as syntax. The sanitising version is the lesson: it blocks the obvious payload and remains fundamentally wrong, because it tries to enumerate dangerous characters instead of keeping data out of the grammar. Every sanitiser is one unconsidered encoding away from failing; parameterisation cannot fail this way because the structure is fixed before the data is supplied.

Step 4 — manufacture a leaked score (7 min)

# leakage.py
import numpy as np
 
rng = np.random.default_rng(0)
n, d = 200, 500                       # more features than samples: pure noise, no signal
X = rng.normal(size=(n, d))
y = rng.integers(0, 2, size=n)        # labels independent of X by construction
 
def cv_score(Xs, ys, folds=5):
    idx = rng.permutation(len(ys))
    scores = []
    for f in range(folds):
        test = idx[f::folds]
        train = np.setdiff1d(idx, test)
        Xtr = np.c_[np.ones(len(train)), Xs[train]]
        theta, *_ = np.linalg.lstsq(Xtr, ys[train], rcond=None)
        pred = (np.c_[np.ones(len(test)), Xs[test]] @ theta) > 0.5
        scores.append((pred == ys[test]).mean())
    return float(np.mean(scores))
 
# WRONG: select features using ALL the labels, then cross-validate.
corr = np.array([abs(np.corrcoef(X[:, j], y)[0, 1]) for j in range(d)])
top = np.argsort(corr)[-20:]
print(f"leaky   (select on all data, then CV): {cv_score(X[:, top], y):.3f}")
 
# RIGHT: selection happens inside each fold, using only that fold's training labels.
idx = rng.permutation(n)
scores = []
for f in range(5):
    test = idx[f::5]
    train = np.setdiff1d(idx, test)
    c = np.array([abs(np.corrcoef(X[train, j], y[train])[0, 1]) for j in range(d)])
    sel = np.argsort(c)[-20:]
    Xtr = np.c_[np.ones(len(train)), X[np.ix_(train, sel)]]
    theta, *_ = np.linalg.lstsq(Xtr, y[train], rcond=None)
    pred = (np.c_[np.ones(len(test)), X[np.ix_(test, sel)]] @ theta) > 0.5
    scores.append((pred == y[test]).mean())
print(f"honest  (select inside each fold): {np.mean(scores):.3f}")
print("true signal in this data: none — labels were generated independently of the features.")

Expected outcome: the leaky procedure reports an accuracy comfortably above chance on data containing no signal at all, while the honest procedure sits near chance where it belongs. Nothing about the model changed — only whether the feature selection saw the held-out labels. This is the leak that survives code review most often, because the selection step looks like preprocessing rather than like training. The rule that follows: any step that looks at labels is part of the model and must live inside the split.


Common misconception
✗ What most people think

"I sanitise user input — strip quotes, escape angle brackets, reject suspicious strings at the boundary. If nothing dangerous gets in, nothing dangerous can happen downstream."

✓ What is actually true

Sanitisation is the wrong primitive, because injection is not caused by dangerous input — it is caused by data being parsed as code by some interpreter further down. That interpreter might be a query planner, a shell, a template engine, a browser's HTML parser, or a serialiser, and each has a different grammar with different escape rules, so a single boundary filter cannot be correct for all of them. The correct fix is to never build a grammatical construct by concatenating data into it: use parameterised statements so the query structure is fixed before values arrive, pass argument arrays rather than shell strings, and encode output for the specific context it lands in. Filtering also fails in a second way, quietly: it mangles legitimate input, so a user named O'Brien cannot register and nobody connects that bug to the security control that caused it.


Week 17 recall · click to reveal
★ = stretch question

Gap analysis + next week preview · 10 min

  • Did the sanitising function's failure surprise you? If your instinct is still "escape the dangerous characters", that instinct is the vulnerability.
  • Could you read the certificate output without looking anything up? That skill turns a class of outage into a sixty-second diagnosis.
  • Did the leaked score land where you expected? Write down the rule about label-touching steps; it is worth more than any single algorithm.

Next week (S086–S090) stays in machine learning and builds on the scaffold: logistic regression and classification metrics; regularisation and the bias-variance trade; tree-based models and ensembles; feature engineering and preprocessing pipelines; and model evaluation and selection done properly. The split discipline and the loss-versus-metric distinction from this week are what keep all of that honest.


Part of the 6-month evergreen learning plan.