Documentation
¶
Overview ¶
Command csrf inserts a hidden token field into every form that posts.
<form method="post" action="/buy"> -> <form …><input type="hidden"
name="csrf_token" value="…">
A token that is missing from one form is a form that fails, and a token that reaches the wrong origin is the thing the token was protecting against. So this program is the one in this set that refuses rather than guesses, and every refusal is counted: a silent gap in a security control is worse than a loud one.
Three of the four decisions cannot be made at the form's start tag.
Whether the form posts. A method attribute says, and a submit button can override it: <form action="/x"><button formmethod="post"> posts, and the button arrives after the form's start tag - after the only place a first child can be inserted. So the document is read twice. The alternative, inserting into every form including the ones that only ever GET, puts a token in a URL, which is how tokens end up in logs and Referer headers.
Whether the form posts to us. A token is a secret shared between this origin and its own pages, and a form posting to another origin hands it over: the browser will send it, and the site receiving it now has a valid token for a user's session. So a cross-origin action is a refusal, and so is a cross-origin formaction on any submitter - which is, again, evidence that arrives after the start tag.
Whether the field would end up in the form at all. This is the one that has to be measured rather than reasoned about: an insertion goes where the markup says, and tree construction can move it. A form written between a <table> and its first row is such a shape - the field is prepended inside the form and a parser puts it beside the table:
<table><form method=post><tr>… the field is a child of the table <table><tbody><form…><tr>… a child of the tbody <select><form…> a child of the body <table><tr><td><form…> inside the form, as written
Measured against golang.org/x/net/html in differential/table_test.go. A field that is not in the form is not submitted with it, so those shapes are refused and reported: the page needs its markup fixed, and no rewrite can do it from here.
And whether the form has a token already, which is the easy one.
The field goes first, before anything else in the form, so that it is present even in a document that is truncated before the form closes - which is what a failed upstream response looks like.