CS103 · Examination paper
2 hours 30 minutes · 100 marks · pass 40 · Merit 60 · Distinction 70
Open book: your notes, no internet, no AI tooling. Sections A–D are answered on
paper. Section E runs on a laptop in exam103.py with no editor autocompletion.
Time budget. A 20 marks / 28 min · B 20 marks / 28 min · C 20 marks / 28 min · D 20 marks / 28 min · E 20 marks / 28 min · checking 10 min.
Sections A–D are tables, values and one-clause answers. Where a question says “name the status”, the number alone earns half: the reason earns the rest. Diagrams are welcome and earn nothing on their own.
Section A — Route the request (20 marks)
Section titled “Section A — Route the request (20 marks)”A1. (6) A logged-in musician opens
https://musicians.staging.fixthemusic.com/pandemoniums/messages/2806551/thread/?tab=all#top(a) (5) Complete the table: the five parts, and the program that decides each.
| Part | Value | Decided by |
|---|---|---|
| scheme | (a) ____ | (b) ____ |
| host | (c) ____ | (d) ____ |
| path | (e) ____ | (f) ____ |
| query | (g) ____ | (h) ____ |
| fragment | (i) ____ | (j) ____ |
(b) (1) Write the request line and the one header that carries a URL part, exactly as the browser sends them.
A2. (4) Resolve three references against the URL in A1. For each, give the resulting URL and name everything the reference discarded.
(a) 2806552/ (b) ?tab=unread (c) #reply
A3. (6) The main site’s locations, abridged:
location ^~ /blog { root /var/lib/ftm-blog/dist/client; }location ^~ /api-next/faro { proxy_pass http://127.0.0.1:12347; }location /tools/ { proxy_pass http://127.0.0.1:8005; }location ~ ^/act/ { return 404; }location ~ ^/api-next { proxy_pass http://frontend_nextjs; }location ~ ^/api(/|$) { proxy_pass http://backend_main; }location ~ ^/auth/magic/ { proxy_pass http://backend_main; }location / { proxy_pass http://frontend_nextjs; }(a) (4) Name the location that serves each of these on staging.fixthemusic.com,
and what answers it — an upstream, a directory, or nginx itself.
/api-next/faro/collect/api-next/revalidate/act/pandemoniums/api/bands/pandemoniums/
(b) (2) State the four matching rules in the order nginx applies them.
A4. (4) Two requests to the main site both return 404. Their access-log lines differ in one field:
{"request":"GET /act/pandemoniums HTTP/1.1","status":404,"request_time":0.001,"upstream_response_time":"-"}{"request":"GET /pandemoniums/messages/ HTTP/1.1","status":404,"request_time":0.034,"upstream_response_time":"0.031"}For each: which process produced the 404, and what you read next.
Examines. Ch1 A1, A2 · Ch9 A3, A4 · Ch12 A4.
Section B — Read the transcripts (20 marks)
Section titled “Section B — Read the transcripts (20 marks)”For each, name the status, what it means here, and the evidence in the transcript that decides it.
B1. (4)
HTTP/1.1 302 FoundLocation: /accounts/login/?next=/pandemoniums/messages/Set-Cookie: messages="..."; HttpOnly; Path=/Content-Length: 0(a) Which component produced this, and what proves the view never ran? (b) What does the browser send next, and what does the musician see?
B2. (4) A POST to the thread with a session cookie the server accepts
returns 403. Give the two causes this leaves, and the single change to the
request that separates them.
B3. (4) One endpoint, GET /musician-api/v0/messages/<uuid>/, four requests.
Give the status for each.
- No
X-API-Token. - Band B’s token, Band A’s uuid.
- Band A’s token,
not-a-uuid. - 30 requests per second to
/apifrom one address.
B4. (4)
OPTIONS /api/bands/search HTTP/1.1Host: staging.fixthemusic.comOrigin: https://example.testAccess-Control-Request-Method: GETHTTP/1.1 204 No ContentAccess-Control-Allow-Origin: https://example.testAccess-Control-Allow-Methods: GET, POST, OPTIONSThe allow-list echoes whatever Origin arrives. State what that grants, why a
single request from the legitimate caller cannot reveal it, and what a second
request would have to change.
B5. (4) Give the value of each, or the error.
const rows = [{ id: "2806551" }, { id: "2806552" }];const handlers = [];for (var i = 0; i < rows.length; i++) { handlers.push(function () { return rows[i].id; });}console.log(handlers.length); // (a)console.log(handlers[0]()); // (b)
const obj = { id: 7, load: () => this.id };console.log(obj.load()); // (c)(d) Change one token in the loop so that every handler returns its own row’s id.
Examines. Ch5 B1, B3 · Ch8 B2, B4 · Ch10 B3 · Ch3 B5.
Section C — Caching, DNS and TLS decisions (20 marks)
Section titled “Section C — Caching, DNS and TLS decisions (20 marks)”C1. (6) Give the exact Cache-Control you would set, and justify each
directive in one clause.
(a) A public band profile page at /<slug>.
(b) Browse results at /api/bands/search?q=jazz.
(c) A musician dashboard thread.
C2. (4) A response carries
Cache-Control: public, max-age=0, s-maxage=60, stale-while-revalidate=300Reusable without contacting the origin? Answer yes or no for each.
(a) CDN, Age: 59 (b) CDN, Age: 60 (c) browser, Age: 0
(d) CDN, on Cache-Control: private, s-maxage=60, Age: 0
C3. (5) Three symptoms, one user each. Name the layer — DNS, TCP, TLS or upstream — give one command that proves it, and state what the output would show.
(a) curl: (6) Could not resolve host
(b) SSL: certificate subject name does not match
(c) curl: (7) Failed to connect: Connection refused
C4. (5) A band renames itself, changing its slug from pandemoniums to
pandemonium. It appears in the wedding-bands category.
(a) (2) Name every surrogate key whose stored objects are now stale. (b) (2) Name the two ISR tags to revalidate. (c) (1) Name the page a URL-only purge leaves stale, and for how long.
Examines. Ch6 C1, C2 · Ch7 C3 · Ch11 C4.
Section D — Diagnose and secure (20 marks)
Section titled “Section D — Diagnose and secure (20 marks)”D1. (6) This view returns another band’s message to any caller holding a valid token.
def get_message(request, internal_id: UUID): msg = EnquiryMessage.objects.get(pk=internal_id) if msg.band != request.auth_api_band: return 403, {"detail": "Forbidden"} return _msg_to_full(msg)(a) (4) Rewrite the body so the wrong object cannot be loaded.
(b) (1) Why 404 rather than 403?
(c) (1) What does .get() raise when the row is absent, and why does that
matter for the rewrite?
D2. (4) A thread template renders an enquiry body with {{ body|safe }}.
(a) Name the vulnerability class. (b) Write a payload that proves it, in the body of a message a client could send. (c) Name the replacement filter. (d) Name the function that decides what survives, and the library it calls.
D3. (4)
.threads .thread { padding: 1rem; }#hero .thread { padding: 0; }article.thread > p { padding: 2rem; }(a) (2) Give the specificity tuple of each rule and the winner for
<p class="thread"> inside <article class="thread"> inside #hero.
(b) (2) Two cards carry width: 50%; padding: 1rem; border: 1px inside a 640px
container, and the row scrolls sideways. Give the width the row occupies, and
the one declaration that fixes it.
D4. (6) Three incidents, three artefact sets. For each, name the hop that answered and the field that proves it.
HTTP/2 200x-cache: HITage: 5400surrogate-key: flatpage{"request":"GET /pandemoniums/ HTTP/1.1","status":404,"body_bytes_sent":153,"upstream_response_time":"0.004"}HTTP/2 200x-cache: MISSx-proxy-cache: HITage: 3(i) A page deleted an hour ago is still live. (ii) A band page 404s, and the Django log has no line for the path. (iii) A band page is served in 4 ms with no origin request.
Examines. Ch10 D1, D2 · Ch2 D3 · Ch12 D4 · Ch6 D3.
Section E — Practical, on the laptop (20 marks)
Section titled “Section E — Practical, on the laptop (20 marks)”Work in exam103.py. Every answer must run. Paste real output where asked.
The location list is the one in A3.
E1. (7) Write pick(path) returning the name of the thing that serves the
path — blog, faro, tools, act-404, api-next, backend_main,
frontend_nextjs — implementing exact, ^~, ordered regex and longest-prefix in
that order. Then paste its output for the four paths in A3 plus
/blog/post and /tools/health.
E2. (4) Write three tests: one exact-prefix case, one where ^~ suppresses a
later regex, one where the earlier of two matching regexes wins.
E3. (3) Paste the failure from a fourth case you believe reveals a defect in
your own pick, and state in one sentence whether the code or the expectation
is wrong.
E4. (6) Write the guard a fragment loader needs:
async def read_html(res): """Return res.text(), or raise with the status in the message."""and the fetch call that uses it with a 5-second timeout. Then paste the output
of two stub cases: ok: False, status: 500 and ok: True, status: 200. No
network.
Examines. Ch9 E1, E2, E3 · Ch4 E4.
Mark scheme
Section titled “Mark scheme”| Section | Marks | Allocation rule |
|---|---|---|
| A | 20 | A1: 1 per table row (5), 1 for the request line with Host (1). A2: 2 for 2806552/ — the path reference also drops the query — 1 each for the other two. A3(a): 1 per path, and a path named without its upstream scores 0 for that path. A3(b): 2 for the order, 1 if ^~ is omitted. A4: 2 per line — 1 for the process, 1 for the next artefact. |
| B | 20 | B1: 1 for the component, 1 for the empty body plus next=, 2 for the GET replay. B2: 1 per cause, 2 for the discriminating field. B3: 1 per status, no half marks. B4: 2 for the universal grant, 1 for why one request looks correct, 1 for the second Origin. B5: 1 for 2, 1 for the TypeError, 1 for the second TypeError, 1 for let. |
| C | 20 | C1: 1 per directive that is right, 1 per page for the justification; a page with public on the dashboard scores 0 for that page. C2: 1 each, and (b) and (d) are the two that separate a reader from a guesser. C3: 2 for DNS, 2 for TLS, 1 for TCP; the layer alone earns half of the sub-item. C4: 1 per key pair (2), 1 per tag (2), 1 for the category page with 86400. |
| D | 20 | D1: 3 for scoping the queryset before the lookup, 1 for returning 404 from the empty result, 1 for why 404, 1 for DoesNotExist. A rewrite that keeps the unscoped get scores at most 2. D2: 1 per part. D3: 1 per correct tuple set, 1 for the winner, 1 for 708, 1 for border-box. D4: 2 per incident — 1 for the hop, 1 for the field. |
| E | 20 | E1: 4 for the algorithm (exact, ^~ early return, ordered regex, prefix fallback), 3 for six correct output lines. E2: 4 = 1 per test that runs and asserts, 1 for the ^~ case being a real suppression test. E3: 2 for a real pasted failure, 1 for the adjudication. E4: 3 for the code, 2 for the two stub results, 1 for the pasted output. |
Expected answers.
- A1 —
https/ browser;musicians.staging.fixthemusic.com/ DNS then the vhost;/pandemoniums/messages/2806551/thread// the router;tab=all/ the view;top/ the browser only. Request lineGET /pandemoniums/messages/2806551/thread/?tab=all HTTP/1.1, plusHost: musicians.staging.fixthemusic.com. - A2 —
…/messages/2806552/(query and fragment dropped);…/thread/?tab=unread(fragment dropped);…/?tab=all#reply(nothing above the fragment changes). - A3 —
farovia^~, because the early return stops before~ ^/api-next;api-next; nginx itself,return 404;backend_main. Order: exact, longest prefix,^~stops, else first matching regex in file order, else the prefix. - A4 — first: nginx alone,
"-"inupstream_response_time, check the location table; second: an application,0.031is the app’s time, read its log. - B1 — auth middleware, before the view; empty body and
next=; aGETof the login page, and after login the redirect target. - B2 — missing or stale CSRF token, or the band does not own the object. Add
csrfmiddlewaretoken: 302 means CSRF, 403 again means authorisation. - B3 — 401, 404, 422, 429.
- B4 — every origin on the internet may read the response; the caller’s own
Originis reflected, so every check it can run passes; a second request with a differentOriginstill receives a matching header. - B5 —
2;TypeError: Cannot read properties of undefined (reading 'id'), becauseiis2after the loop;TypeError, because the arrow readsthisfrom module scope, where it isundefined;let i. - C1 —
public, max-age=0, s-maxage=60, stale-while-revalidate=300;public, max-age=0, s-maxage=300, stale-while-revalidate=600;private, no-store. - C2 — yes; no, stale at exactly the lifetime; no,
max-age=0; no,privaterefuses shared storage before the lifetime matters. - C3 — DNS,
dig +short <host>,NXDOMAINor empty; TLS,openssl s_client -servername <host>, the SAN list does not cover the name; TCP,curl -vornc -z, nothing listening on the port. - C4 —
profile-pandemoniums,profiles,profile-pandemonium,landing landing-category-wedding-bands;revalidateTagon both slugs;/hire/wedding-bands/pandemoniums,max-age=86400. - D1 —
_get_user_messages_qs(request).filter(internal_id=internal_id).first(), then404onNone. 403 confirms the object exists;DoesNotExistmeans the unscopedgetraises before any check runs. - D2 — stored XSS;
<img src=x onerror=fetch('/musician-api/v0/messages/')>;safe_html;sanitize_content_html, which callsnh3.clean()against an allow-list. - D3 —
(0,2,0),(1,1,0),(0,1,2);#hero .threadwins at the first column. 708px, becausewidthis the content box and padding plus border are added;box-sizing: border-box. - D4 — (i) Fastly,
age5400 against a deletion an hour old, andsurrogate-key; (ii) an application,0.004is numeric so nginx proxied, and the absent Django line means Next.js answered; (iii) nginx’sproxy_cache,x-proxy-cache: HITwithage3. - E — the algorithm is the chapter’s
pick;faromust beatapi-next;/act/pandemoniumsreturnsact-404. E4:if not res.ok: raise RuntimeError(str(res.status)),signal: AbortSignal.timeout(5000).
Grade descriptors
Section titled “Grade descriptors”| Band | The script |
|---|---|
| Distinction (70+) | A3 right on all four paths, including faro beating api-next; A4 names the field that separates the two 404s. B2 gives both causes and the one field that discriminates. C1 exact, with s-maxage and stale-while-revalidate on the public pair and private, no-store on the dashboard. C2 right at the lifetime boundary and on private, s-maxage. D1 scopes the lookup instead of checking after it. D4 cites a field per incident. E runs, and the failing test fails for the stated reason. |
| Merit (60–69) | One path wrong in A3, usually api-next/revalidate sent to faro. C1 values right, justification “because it is public” without the cache-key reason. C2 wrong only at Age: 60. D1 returns 404 but by catching DoesNotExist around an unscoped get. E tests pass but none of them fails. |
| Pass (40–59) | A1 complete but the fragment marked as transmitted. B3 swaps 404 and 403 for the other band’s uuid. C3 names the layer but offers no command. D3 computes the tuples and then picks the winner by counting classes. D4 names Fastly for (i) without the age argument. E runs but output is not pasted. |
| Fail (<40) | A3 by “the most specific location wins”. B1 answers “403 means not logged in”. C1 reaches for no-cache where the body must not be stored at all. D1 adds a permission check after the lookup. E does not run. |
Section floors. A ≥ 8/20, D ≥ 8/20, E ≥ 8/20. A candidate who cannot route a request, secure an object, or make their own code run has not met the module’s outcome, whatever the aggregate.
Boundaries. Pass 40, Merit 60, Distinction 70, each on the aggregate after the floors are applied.