CSE 134B Web Client Languages
  1. Interaction
  2. Forms
  3. Form Security

Interaction · Forms 05

Form Security

A form runs on a machine that belongs to someone else, and what it sends is only text that claims to come from it. Everything the browser checks is a service to the person filling it in; nothing it checks is a promise to the server. This page shows how easily the checks come off, what the server has to do instead, and what the page itself can still get right: HTTPS, the method, passwords, and a fair answer to bots and agents.

Form Accessibility and Usability added constraints to a form: required, type, pattern, min and max, maxlength. Each one catches a mistake in the second it is made, next to the field that caused it, and the page argued for using all of them. This page is about what they are not. It works through one checkout form, Trail Shop's order page, that looks locked down and is not, and then follows the order across the network to the place where the real decisions have to be made. Foundations §16 made the short version of the argument; this is the long one, form by form.

1. A form is not a fence

Every attribute in a form is an instruction to the browser that rendered it. The browser follows the instructions faithfully, for the person using it, as long as the person lets it. But the page is a copy, running on the person's own machine, and the person can change the copy. There are three easy ways around any constraint, and none of them takes skill.

  • Edit the page. Open DevTools, choose the Elements panel, and delete the attribute. required is gone in one keystroke; a hidden field's value is a double-click away. The browser now enforces the edited page, because that is the page it has.
  • Run a script. The console, a bookmarklet or a browser extension can set form.noValidate = true, change any value, or skip the form and send the request with fetch(). Any script on the page can do the same, including third-party scripts the site itself invited in.
  • Skip the browser. curl, a Python script or a bot builds the HTTP request directly. It never downloads the form, so it never learns that a constraint exists.

The table goes through the attributes people most often mistake for protection.

MarkupWhat it does for the personHow it is bypassed
required, pattern, type, min/max/stepCatches a mistake before the round trip, with a message beside the fieldDelete the attribute, add novalidate, or send the request without the form
maxlengthStops typing at the limit, so the person sees it instead of an error laterDelete it, or send any length; the server's own limit is the real one
type="hidden"Carries a value the person need not see or editView Source shows it; DevTools edits it; it is sent like any field
readonlyShows a value that is submitted but not meant to be typed overDelete the attribute and type, or edit the request
disabledShows a choice that does not apply now; the control is not submittedDelete the attribute, and the control is submitted like any other
A <select> or radio setOffers only the valid choicesSend any value under that name; the server sees text, not a menu
A total computed in the pageShows the person what they will payEdit the field, or send a different number

None of this makes client-side constraints pointless. They make the common case fast and pleasant for every honest person who fills the form in, and those people are nearly everyone. Client-Side Communication, step 01 puts it exactly: constraint validation is a user-experience feature, not a boundary. The mistake is only in believing that a constraint the server did not check again was ever checked at all.

Three routes to the same serverThree senders on the left. The browser with the form as written sends price=129.00 and qty=1. The browser after DevTools edits sends price=0.01 and qty=-3. Curl, with no browser and no page, sends price=0.01 and qty=500. All three arrows arrive at one server box, which receives only name and value text and cannot tell which route a request took.the form as writtenconstraints checkedbrowser + DevToolsconstraints deletedcurlno browser, no pageprice=129.00&qty=1price=0.01&qty=-3price=0.01&qty=500serverreceives name=value textand the headers sentcannot see the routeor the page it came fromthe request carries no proof of how it was made; the server decides what to accept
Three ways to send an order. The server receives the same kind of request from each, and has to judge it on its contents alone.

2. The tampered checkout

Trail Shop's checkout looks careful. The product comes from a <select>, so only real products can be chosen. The quantity is a number from 1 to 5. The unit price sits in a hidden field, set by the page when the product changes. The total is readonly, calculated by the page. The name and coupon fields have maxlength, the coupon has a pattern, express shipping is a disabled option for members, and the terms box is required. Here is the part that matters:

<select id="product" name="product" required> <option value="boots-trail" data-price="129.00" selected>Trail boots, $129.00</option> … </select> <input type="hidden" id="price" name="price" value="129.00"> <input id="qty" name="qty" type="number" required min="1" max="5" value="1"> <input id="total" name="total" type="text" readonly value="129.00">

The demo is that checkout. It posts to /api/echo, the course's endpoint that replies with exactly the request it received, so you can see what a server would see. Fill it in honestly once, then follow the steps under the form.

The echo comes back with the tampered values in the body, as plain as the honest ones:

{ "method": "POST", "path": "/api/echo", "headers": { "content-type": "application/x-www-form-urlencoded", … }, "bodyKind": "urlencoded", "body": "product=boots-trail&price=0.01&qty=-3&total=0.01&shipping=express&name=Pat&coupon=" }

There is no consent field at all, because an unticked checkbox sends nothing, and with required deleted nothing stopped the order. The quantity is negative. The shipping is a choice the page never offered. And there is nothing in the request to say any of it was edited.

The same order with no page

DevTools is the slow way. The fast way never opens the page. This command, which you can run in any terminal, sends the order straight to the echo endpoint:

curl https://cse134.site/api/echo \ -d product=boots-trail -d qty=1 \ -d price=0.01 -d total=0.01 \ -d shipping=express -d name=Pat

Each -d adds one name=value pair to the body. With -d, curl sends a POST with Content-Type: application/x-www-form-urlencoded, the same encoding the form uses, so the echo reports "bodyKind": "urlencoded" and a body of product=boots-trail&qty=1&price=0.01&total=0.01&shipping=express&name=Pat. The headers differ: there is no Origin, no cookie, and the User-Agent says curl. But headers are text the sender writes too; -H 'User-Agent: …' makes curl claim to be any browser.

DevTools and curl are not attack tools; every web developer uses both daily. That is the point: the means to send any request are on every machine.

3. The receptionist and the security guard

An office building has a receptionist and a security guard, and they do different jobs. The receptionist is there to help: tells visitors which floor to go to, points out that they forgot to sign in, hands them a pen. The security guard is there to decide: checks the badge, and does not care how helpful the receptionist was. A visitor who walks past the receptionist, or comes in by the loading dock, still meets the guard.

Client-side validation is the receptionist. It is friendly, fast and close to the person, and it catches the honest mistakes, which are almost all of them. Server-side validation is the guard. It sits where the visitor cannot reach, checks everything that matters, and is never skipped. A building with only a receptionist has no security; one with only a guard is safe but miserable, every forgotten field costing a round trip and, too often, a form that comes back empty. Good forms have both, and give them the same rules.

The receptionist on the client, the security guard on the serverOn the left, the receptionist, the client: required, type, pattern, min, max, step, maxlength, readonly, disabled and hidden; it checks as you type, helps honest people, and can be removed by anyone. A dashed trust boundary in the middle. An arrow crosses it labeled text. On the right, the security guard, the server: the same rules again, price from the catalog, encode output and bind queries, CSRF token and rate limits; it cannot be skipped.receptionist: the clientrequired, type, patternmin, max, step, maxlengthreadonly, disabled, hiddenchecks as you type: fasthelps honest peopleanyone can remove ittrust boundarytextguard: the serverthe same rules, againprice from the catalogencode output, bind queriesCSRF token, rate limitsdecides what is acceptedcannot be skipped
Client checks serve the person; server checks protect the data. Only text crosses the boundary between them.

"The same rules" is a real requirement, not a slogan. If the page allows a quantity up to 5 and the server allows up to 3, honest people get past the page and are then refused by the server, with a message they could not have predicted. If the server allows more than the page, the page is lying about the rules. Many teams write each rule once, in a shared schema, and generate both checks from it.

4. What the server must do

Writing the server side is the subject of CSE 135, which builds the endpoints this course's forms post to. What the form author needs to know is what the server's half of the contract is, because the form is designed around it. There are six parts.

Revalidate everything

Every rule the form expresses is checked again on the server: present, the right shape, in range, one of the allowed values, no longer than the limit. Then the server checks the rules the form cannot express, the ones that need data the page does not have: the product exists and is in stock, the coupon is real and unexpired, this account may use express shipping. When a check fails, the server answers with a message per field and sends the form back with what the person typed still in it, so the receptionist's work is not undone by the guard.

Look prices up; never read them from the form

The fix for the tampered checkout is not a cleverer hidden field. It is to stop asking the client for facts the server already knows. The form should send what the person chose, the product's id and the quantity, and the server should look up everything else from its own records. The hidden price and the readonly total can stay in the page as a preview for the person, or go, but the server never reads them.

// Prices in cents, from the shop's own database, never from the request. const CATALOG = { 'boots-trail': 12900, 'pack-25': 8900, 'bottle-1l': 2400 }; function priceOrder(fields) { // fields: the parsed form body const product = fields.get('product'); const unitCents = CATALOG[product]; if (unitCents === undefined) return { error: { product: 'Choose a product.' } }; const qty = Number(fields.get('qty')); if (!Number.isInteger(qty) || qty < 1 || qty > 5) { return { error: { qty: 'Order 1 to 5.' } }; } if (fields.get('consent') !== 'yes') { return { error: { consent: 'Agree to the terms to place the order.' } }; } // fields.get('price') and fields.get('total') are never read. return { product, qty, totalCents: unitCents * qty }; }

The same rule covers every value the server can derive: the logged-in user's id comes from the session, not a hidden user_id field; a discount comes from the coupon table, not a discount field; a role comes from the account, not from ?role=admin. If a field could change what someone is allowed to do or what they pay, the server should not be accepting it from the client at all.

Treat every field as untrusted text

HTTP has no numbers, dates or booleans. Whatever the form's type was, every value arrives as a string, and the server has to parse it on purpose: "5", "5.0", "-3", "1e3" and "five" all reach the code that expected a quantity. The server also has to expect fields that the form never had, missing fields that it always had, and the same name sent twice. Accept only the fields you asked for, by name, and ignore the rest; a server that copies every submitted field into a database record will one day copy is_admin=true into one.

Encode on output

A value is dangerous not when it arrives but when it is used somewhere that gives its characters meaning. A name field holding <script>…</script> is harmless text in a database. Written into an HTML page without encoding, it becomes markup, and the script runs in your origin for every visitor who views it. That is cross-site scripting (XSS), and the defense is to encode every value for the place it is going: HTML-escape it in a page, so < becomes &lt;; percent-encode it in a URL. Server template engines do the HTML case by default; the bugs live where someone turned that off. In the browser, the same rule means writing data with textContent, which never parses markup, and not with innerHTML:

// The name came from a form. If it contains markup, the markup runs. greeting.innerHTML = `Thanks, ${order.name}. Your order is on its way.`; greeting.textContent = `Thanks, ${order.name}. Your order is on its way.`;

Security lists where the course covers XSS in depth, from the DOM's parsing methods to the Content Security Policy header that limits what an injected script could do.

Parameterize queries

The same mistake in a database is SQL injection: a value pasted into the text of a query becomes part of the query. A product id of x' OR '1'='1 turns a lookup of one row into a lookup of all of them. The defense is to never build a query by joining strings, and to pass values separately as parameters, which the database treats as data whatever they contain.

db.query("SELECT price_cents FROM products WHERE id = '" + product + "'"); db.query('SELECT price_cents FROM products WHERE id = ?', [product]);

Check that the person meant to send it

A form on any site can post to any other site. If you are signed in to Trail Shop, a page on another site can hold a hidden form whose action is Trail Shop's order endpoint and submit it with a line of script. Your browser sends the request, and, unless something stops it, sends your Trail Shop cookie with it, so the order arrives looking like yours. That is cross-site request forgery (CSRF). The defenses, in outline:

  • A CSRF token. The server puts a random, per-session secret in a hidden field of every form it serves and refuses any state-changing request without the right one. It is a hidden field that works, and the reason is instructive: the server is not trusting the value as data about the order; it is checking that the request came from a page the server itself rendered, because another site cannot read that page to copy the secret.
  • SameSite cookies. A session cookie set with SameSite=Lax is not sent on a cross-site POST, so the forged order arrives without a session. Chrome and Edge treat a cookie with no SameSite attribute as Lax; other browsers do not, so set it explicitly. SameSite is a strong second layer, not a replacement for the token.
  • Checking where the request came from. Browsers send an Origin header on a form POST and a Sec-Fetch-Site header saying whether the request is same-origin, same-site or cross-site; a server can refuse cross-site requests to endpoints that change things.
Set-Cookie: session=8f2c…; Secure; HttpOnly; SameSite=Lax; Path=/

The Browser for Programmers, VII covers the origin, cookie flags and CSRF in the browser's security model. Building the token check is server work, and CSE 135's.

5. HTTPS for every form

A form sends what people type across networks the site does not control: a café's Wi-Fi, a hotel's router, an internet provider, any number of machines in between. Over plain HTTP, every one of them can read the request and change it. HTTPS, HTTP over TLS, closes that gap. It belongs on every page of a site, and every form in particular, not only the login page.

What HTTPS protects

  • Confidentiality in transit. The request and response, URL path and query string included, are encrypted between the browser and the server. A type="password" field only hides the characters from someone looking at the screen; HTTPS is what hides them from the network.
  • Integrity in transit. Nobody on the path can change the page on its way to the browser or the form data on its way back without the change being detected.
  • The server's identity. The certificate shows the browser that it is talking to the host named in the address bar.

Integrity is why HTTPS on the form's page matters as much as on its action. A login form served over HTTP that posts to an HTTPS URL looks safe in the source, but anyone on the network can rewrite the page before it arrives, change the action to their own server, or add a script that copies each keystroke. The page that holds the form has to be as trustworthy as the place it sends the data.

What HTTPS does not protect

  • What the server does with the data. The data arrives safely at the server, and then it is the server's responsibility. HTTPS says nothing about how it is stored, logged or shared.
  • The page itself. Anything that runs in the page sees the form's values in the clear, including every third-party script the page loads and every browser extension the person has installed.
  • That the site is honest. A certificate proves which host you reached, not that the host deserves your password. Phishing sites have valid certificates too; the padlock means "private connection," not "good site."
  • Everything about the connection. The network can still see which server's IP address you connected to, usually the host name, and roughly how much data moved and when.

What the browser tells people

Chrome 56, in January 2017, began labeling HTTP pages with a password or credit card field "Not secure"; Firefox at the same time began showing a crossed-out padlock on such pages. Since Chrome 68, in 2018, Chrome labels every HTTP page "Not secure," and since Chrome 70 it turns the label red when the person types into a field on one. Firefox, since version 52, shows a warning beside a login field on an HTTP page, "Logins entered here could be compromised," at the moment someone clicks into it. And Chrome warns before a form on an HTTPS page submits to an HTTP address, the so-called mixed form, since there the page looks secure and the data is not.

The next step goes further. Google announced in 2025 that Chrome's "Always Use Secure Connections" setting would become the default for public sites in October 2026, with Chrome 154: Chrome tries HTTPS first and asks before loading a public site that only offers HTTP. For a form author the practical rule has not changed in years: serve everything over HTTPS, redirect HTTP to it, and send the Strict-Transport-Security header so browsers never try plain HTTP for your site again.

6. What GET leaks

HTTPS encrypts the URL on the wire, but a URL does not stay on the wire. The form and its request showed that a GET form puts its fields in the query string. Once there, they are copied into places nobody audits:

  • the address bar, where anyone behind the person can read them;
  • the browser's history and its address-bar suggestions, on a shared or borrowed machine;
  • bookmarks and links people share, carrying the query with them;
  • the access logs of the server, the load balancer, the CDN and any proxy in front of them, often kept for months and readable by more staff than the database;
  • analytics tools, which record the full URL of each page view;
  • and the Referer header, under the rules below.

What the Referer header still sends

When a page loads an image, a script or a stylesheet, or the person follows a link, the browser may tell the destination which page the request came from, in the Referer header (misspelled in the standard since 1996). What it sends is set by the page's referrer policy. When a page sets none, every current browser uses strict-origin-when-cross-origin, which sends:

  • to the same origin: the full URL, path and query string included;
  • to another origin over HTTPS: only the origin, such as https://shop.example/;
  • to an HTTP address from an HTTPS page: nothing.
What the default referrer policy sends from a search results pageOn the left, a page reached by a GET search, shop.example/search?q=bankruptcy+lawyer. Arrows to three destinations. Same origin, such as the next page or its own images: the Referer is the full URL with the query. Another HTTPS origin, such as a CDN or analytics: the Referer is only https://shop.example/. An HTTP origin: no Referer at all. A note under the page: any script on the page can still read location.href, query and all.a page reached by a GET searchhttps://shop.example/search?q=bankruptcy+lawyerbut any script on the page can still readlocation.href, query and allsame origin: next page, own imagesReferer: full URL with ?q=another HTTPS origin: CDN, analyticsReferer: https://shop.example/an HTTP origin (a downgrade)no Referer at all
The default policy trims the Referer for other origins, but the query string still goes to your own origin, and the page's scripts can read it directly.

That default, adopted by browsers in 2020 and 2021 in place of the older no-referrer-when-downgrade, stops the worst of the old leak, in which every third-party image on a results page received the full search URL. It does not stop everything. The full URL, query included, still goes with every same-origin request, so it lands in your own logs again and again. The origin still goes to every other site the page loads from, which tells each of them that this visitor was on your site. Any third-party script running in the page does not need the header at all: it reads location.href. And a site can weaken the policy for a page or a single link with unsafe-url, which sends the full URL everywhere. The fragment, the part after #, is never sent.

The rule follows: GET for questions whose answers deserve a URL, such as a product search or a filter on a list; POST for anything sensitive, anything personal, and anything that changes something. A password in a GET form is the classic mistake, and Foundations §16 lists everywhere it ends up. A page whose URL is itself sensitive, such as a search on a health or legal site, can also send a stricter policy, Referrer-Policy: same-origin or no-referrer, as a response header or a <meta name="referrer"> element.

7. Passwords

Password forms are where good intentions do the most harm. The most useful thing a password form can do is get out of the way of the tool that handles passwords best: the password manager built into every browser and the separate ones people install.

Let managers work

A manager generates a long random password for each site, stores it, and fills it in only on the site it belongs to, which also makes it a quiet defense against phishing: it will not fill in a lookalike domain. Managers find their way around a form by its markup, and the autocomplete attribute tells them exactly what each field is:

<form action="/login" method="post"> <form-field> <label for="username">Email</label><br> <input id="username" name="username" type="email" required autocomplete="username webauthn"><br> </form-field> <form-field> <label for="password">Password</label><br> <input id="password" name="password" type="password" required autocomplete="current-password" aria-describedby="password-error"><br> <output id="password-error" class="error" for="password" aria-live="polite"></output><br> </form-field> <button type="submit">Sign in</button> </form>

current-password asks the manager to fill in the saved password; new-password, on sign-up and change-password forms, asks it to offer a new strong one and save it. username marks the field the password belongs to; the extra webauthn token is for passkeys, below. On a change-password form, put the new password in a field marked new-password, and keep a username field in the form, even a read-only one, so the manager knows which account it is updating.

Then avoid the things that break managers and the people who use them. Do not block paste into password fields: it stops managers that paste, and it fails people who cannot remember or retype a long password. WCAG 2.2 success criterion 3.3.8, Accessible Authentication (Minimum), at level AA, does not allow a login step that relies on remembering or transcribing a password unless there is a way around it, and the W3C's guidance for the criterion names supporting paste and password managers as a way to meet it. Do not split a password or a one-time code across several boxes. Do not set a low maximum length. Offer a button that shows the password, so a person typing one by hand can check it.

No composition rules

"At least one capital, one digit and one symbol" feels like security and mostly produces Password1#. The United States' standard for digital identity, NIST Special Publication 800-63B, has said so for years, and its current revision, SP 800-63B-4, published in July 2025, makes it a requirement for the services that follow it. Verifiers:

  • "SHALL NOT impose other composition rules (e.g., requiring mixtures of different character types) for passwords";
  • "SHALL NOT require subscribers to change passwords periodically," only when there is evidence of compromise;
  • require a minimum of 15 characters for a password used on its own, and eight for a password used as part of multi-factor authentication;
  • "SHOULD permit a maximum password length of at least 64 characters" and should accept all printing ASCII characters, the space, and Unicode;
  • compare each new password against a blocklist of "commonly used, expected, or compromised" values;
  • "SHALL allow the use of password managers and autofill functionality" and "SHOULD permit claimants to use the 'paste' function."

In the form, that means a long minimum, stated in the hint; no pattern that demands character classes; and a check against breached and common passwords on the server, which the form can only report. Length is the only rule a person needs to be told in advance.

<form-field> <label for="new-password">Choose a password</label><br> <input id="new-password" name="password" type="password" required minlength="15" autocomplete="new-password" aria-describedby="new-password-hint new-password-error"><br> <output id="new-password-hint" class="hint" for="new-password">At least 15 characters. Spaces and any characters are fine, and so is your password manager.</output><br> <output id="new-password-error" class="error" for="new-password" aria-live="polite"></output><br> </form-field>

Passkeys, in outline

The better answer is no password at all. A passkey is a key pair created by the person's device for one site: the private key stays on the device, or in the person's password manager, and the site stores only the public key. To sign in, the device proves it holds the private key by signing a challenge from the server, after the person unlocks it with a fingerprint, face or device PIN. Nothing reusable crosses the network, there is no password on the server to steal, and the key is bound to the site's origin, so a phishing page on another domain cannot use it. Passkeys are built on the Web Authentication API (WebAuthn), which is available in all major browsers. The webauthn token in the sign-in form above lets the browser offer saved passkeys in the same autofill menu as saved passwords. The ceremony itself is JavaScript and server work; the form's part is to leave room for it.

8. Bots and automation

Any form on the public web will be found by programs. Some are welcome, like a search engine or an accessibility checker. Some fill in the contact form with spam, try leaked passwords against the login form a thousand at a time, create fake accounts or buy up limited stock. The first question is what you need to stop, and how badly; the second is what it costs the people you do not want to stop. Start with the measures that cost people nothing, and climb only as far as the problem forces you to.

A honeypot field

Simple bots fill in every field they find. A honeypot is a field people are told to leave empty, hidden from sight, which a careless bot fills in. A filled honeypot is a strong hint that no person filled in the form.

<form-field> <label for="leave-empty">Leave this field empty</label><br> <input id="leave-empty" name="leave-empty" type="text" tabindex="-1" autocomplete="off"><br> </form-field> @layer components { form-field:has(input[name="leave-empty"]) { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } }

The details are about people. The label tells the truth, so a screen reader user who reaches the field hears what to do. tabindex="-1" keeps it out of the keyboard's tab order; autocomplete="off" and a name no autofill would recognize keep a browser from filling it in. Even so, treat a filled honeypot as a signal, not a verdict: hold the submission for review, or ask for one more step, rather than silently throwing away a real person's message.

Timing and behavior signals

A server can learn a lot without asking anything. It can put the time the form was served into a signed hidden field and notice an order placed half a second later. It can notice a request with no cookies, no Origin, or headers in an order no browser sends. It can notice a hundred sign-ups from one address range. Each signal is weak alone and wrong sometimes, since password managers and people who know what they want are fast too, so signals are combined and used to raise suspicion, not to refuse outright.

Rate limits

Most automated harm is a matter of volume: guessing passwords, testing stolen card numbers, flooding a form. Limiting how often an account, an email address or a network address may try, and answering the excess with 429 Too Many Requests and a Retry-After header, turns a fast attack into a slow one and costs a person almost nothing. A login form that allows five tries a minute per account has defeated most password guessing without anyone noticing.

A challenge, last

Only when a form is under real, targeted attack, and the cheaper measures have failed, is a challenge, a CAPTCHA, worth its price. And the price is paid by people, not bots. Image puzzles block people who are blind or have low vision; audio versions block people who are deaf or in a noisy place; distorted text and timed puzzles block people with cognitive disabilities and people on slow connections; and modern bots solve many puzzles faster than people do. The W3C's note Inaccessibility of CAPTCHA sets out the problems and the alternatives. Invisible, score-based challenges avoid the puzzle but send data about the visitor to a third party, which is a privacy cost the person never agreed to. If you must challenge, challenge only the suspicious requests, offer more than one way through, and never put a challenge in front of everyone because of what a few might do.

The principle behind this ordering is the one the web's own standards are built on. The W3C's design principles call it the priority of constituencies: "If a trade-off needs to be made, always put user needs above all," and then "user needs come before the needs of web page authors, which come before the needs of user agent implementors, which come before the needs of specification writers, which come before theoretical purity." A puzzle shown to every visitor because it was the easiest thing for the developer to add puts the author's convenience above the user's needs, exactly backwards. Most sites do not face the bots a large email provider faces, and should not make visitors pay as if they did.

9. AI agents that fill in forms

A newer kind of automation does not attack forms; it fills them in for someone. Browsers and assistants now include AI agents that, asked to "book a table for four on Friday" or "renew the parking permit," open the site, read the page, type into the fields and press the button, in the person's own browser and often in their signed-in session. For some people, including many with disabilities, that help is a real gain in independence. For a site, it is a new kind of visitor that is neither a person at a keyboard nor a hostile bot.

What changes

  • Behavior signals blur. An agent in a real browser has real cookies, a real Origin and a plausible pace. Timing checks and honeypots, which a careful agent reads and obeys like a person would, will not reliably tell it apart, and should not be expected to.
  • The session is the person's. A request an agent makes in the person's signed-in browser is a same-site request with a valid CSRF token. Those defenses protect the person from other sites; they were never meant to tell the person from their own tools.
  • Mistakes arrive with confidence. An agent can misread a field, pick the wrong date or order twelve instead of two, and it will not hesitate as a person might. Consequential actions need a step that shows what is about to happen: a review page with the order, the total and the date, which the person, or an agent asked to confirm with them, has to accept.
  • Some agents will say who they are. The IETF's Web Bot Auth working group is drafting a way for automated clients to sign their requests so a server can verify which agent sent them. It is a draft, and an unsigned request proves nothing either way.

What does not change

The server still decides. An agent sends the same name and value text as a person, a script or curl, and the server checks it the same way: revalidate every field, look up every price, check that this account may do this thing, limit the rate. Everything in sections 4 and 8 applies unchanged; nothing about agents makes the client more trustworthy, and nothing makes it less, because it was never trusted.

A well-built form also serves agents the way it serves everyone else. Agents read the same markup that screen readers and password managers read: labels tied to controls, fieldsets with legends, autocomplete tokens, messages in <output> elements linked with aria-describedby. A form that a screen reader can describe is a form an agent can fill in correctly, and fewer of its mistakes reach the server.

The temptation is to answer agents with forms only a human can complete: puzzles, tricks, fields that confuse automation. Resist it unless an action truly must be done by a person in person, and then use a deliberate step such as re-authentication or a confirmation sent to the account owner, not a puzzle. A test designed to tell people from software will also stop the people who rely on software to use the web at all, and it fails the priority of constituencies for the same reason a challenge in front of every visitor does.

10. Summary

ConceptKey points
Not a fencerequired, pattern, min/max, maxlength, hidden fields, readonly and disabled are removed in DevTools, changed by script, or skipped by curl. They serve the person; they protect nothing.
The tampered checkoutA hidden price of 0.01 reaches the server as plainly as 129.00, with no sign it was edited. The request carries no proof of how it was made.
Receptionist and guardClient checks help honest people fast; server checks decide. Both use the same rules; the server's copy counts.
The server's jobRevalidate everything; look prices and permissions up by id, never read them from the form; parse every field as untrusted text and accept only expected names; encode on output; parameterize queries; CSRF tokens and SameSite cookies. Taught in CSE 135.
HTTPSEvery page, every form. Protects confidentiality and integrity in transit and the server's identity; not storage, scripts in the page, or honesty. Browsers mark HTTP "Not secure" and warn on password fields and mixed forms.
GET leaksQuery strings land in history, bookmarks, logs and analytics. The default strict-origin-when-cross-origin still sends the full URL to the same origin and the origin to others. POST for anything sensitive.
PasswordsLet managers work: autocomplete username, current-password, new-password; allow paste; no composition rules or forced changes (NIST SP 800-63B-4); a long minimum and a breach blocklist. Passkeys remove the password altogether.
BotsHoneypot, timing and behavior signals, rate limits, and a challenge only as a last resort for suspicious requests. Put user needs first: the priority of constituencies.
AI agentsAgents fill forms for people in real browsers and sessions; signals blur and mistakes need a review step. The server still decides. Good markup helps agents fill correctly; human-only forms exclude people.