The Field That Wins: Two Inputs, One Name, One Answer
The challenge
Brackwell Council pays its volunteers a stipend by the hour. One volunteer's payments have not matched their printed timesheet for four months: the sheet the supervisor signs says one number, the payment run says another, and the portal shows the signed number on every screen anybody looks at. Nobody has found a way to edit a submitted sheet. This is the page as the browser had it when the form was sent. Read what the form would actually post and give the number of hours the server accepted.
What you'll learn
- Spot duplicate parameter names in a form's source
- Explain how different frameworks resolve a repeated HTTP parameter
- Separate the value a page renders from the value it posts
- Recognise a reporting field that reads different data from the one that is submitted
- Describe why an internally consistent interface can still be wrong
Skills tested
Prerequisites
- What an HTML form and a hidden input are
How it works
HTTP has no rule that a parameter appears once. A form with two inputs sharing a name posts both, and the request is perfectly well formed. What the application sees depends entirely on how its framework resolves the repetition, and the frameworks disagree: PHP and Rails keep the last, Express with its default parser produces an array, Flask returns the first through bracket access and the full list through getlist, and some load balancers and web application firewalls parse it differently again from the app behind them.
That disagreement is the bug class. It matters most when one occurrence is visible and under the user's control and the other is hidden and carries a value that means money, permission or identity. The fix people reach for, hiding the field, does nothing: a hidden input is neither secret nor read-only, it is just not painted.
The second half of this one is worth as much as the first. The reports read hours_display, a different field with a different value, so every human-facing surface agreed with itself for four months. When two code paths read two fields that are supposed to hold the same thing, they will eventually disagree, and the one nobody looks at is the one that decides.
Common mistakes
- Answering 12. That is the visible control and the signed figure, which is exactly why the discrepancy went unexplained for four months.
- Assuming the last field wins. True in PHP and Rails. The comment names Werkzeug, where bracket access returns the first.
- Answering 418.
total_hours_ytdis a running total, and it is not the field being posted for this week. - Reading only the preview. Three of the fields that matter are never painted.
How to defend against it
Reject ambiguity instead of resolving it.
- Treat a repeated parameter as a malformed request. Read with
getlist, and if the list has more than one entry, return a 400 and log it. - Never send a value the server can compute. The hours that get paid should come from the submitted control alone, validated against the supervisor's approval record on the server.
- Have one field of record. Reports, exports and the payment run should all read the same stored value, so a disagreement is impossible rather than invisible.
- Cross-check the outputs. A weekly report of paid hours against approved hours would have caught this in the first payment run.