1. Introduction
This section is not normative.
A user’s credentials are valuable, and are often the key target of phishing and content injection attacks.
Users can defend themselves from the former threat by using a credential manager which enables the generation of strong, unique passwords for all the sites they visit, and which only provides those credentials to the website they’re tied to. If a user relies on a credential manager to keep track of her passwords, and the credential manager refuses to fill them into forms on unexpected origins, phishing becomes significantly more difficult.
Content injection, on the other hand, can turn the credential manager against
the user, as demonstrated in "Automated Password Extraction Attack on Modern
Password Managers" [LUPIN]. If an attacker can inject a form into a website
and the credential manager can be tricked into filling it, the user’s
credentials will be available to the attacker directly via DOM APIs or
indirectly via form submission to a malicious endpoint. The latter risk can be
mitigated via the Content Security Policy form-action
directive [CSP3], but the former is a real problem. If passwords are a simple "value"
accessor away, users are at risk.
It is tempting to simply block read access to password fields, but many websites do interesting things with password fields. They increasingly read credential values in order to sign a user in via XMLHttpRequest, for instance, rather than submitting a form. A blanket restriction is therefore unlikely to be web-compatible.
Instead, this document proposes an opt-in mechanism by which a website can choose to deny itself read access to the values of particular form fields. User agents could (and should!) allow credential managers to help users sign-in by writing values into form fields, but those values would remain opaque to the website’s JavaScript.
1.1. Examples
1.1.1. Sign-in Form
<form action="/signin-endpoint" method="POST"> <input type="text" autocomplete="username" name="username"> <input type="password" writeonly autocomplete="current-password" name="password"> <input type="submit"> </form>
1.1.2. Content Security Policy
Content-Security-Policy: form-writeonly current-password new-password;
Note: A real page’s policy should also include form-action and connect-src directives to mitigate the risk of exfiltration.
2. Preventing DOM access to form fields
Developers may opt-into preventing DOM access to form fields by setting a writeonly
attribute on specific form or form-associated elements, or by using the form-writeonly
Content Security Policy directive to
prevent DOM access to any and all form-associated elements based on
their autocomplete
attributes. Both mechanisms are
described below:
2.1. The writeonly
attribute
writeonly
is a boolean attribute present on form, input, select, and textarea elements that controls
whether or not an element’s value may be read directly via the value
IDL attribute or content
attribute, or inferred by statically validating the
constraints of a form
. The attribute’s formal definitions are below, in §2.1.1 writeonly IDL.
At a high level:
-
Adding the
writeonly
attribute to an element will set that element’s write-only value flag. -
If a
form
'swriteonly
attribute is set, then set the write-only value flag for allinput
,select
, andtextarea
elements associated with that form. -
If an element’s write-only value flag is set, then JavaScript access to the element’s value is blocked in a number of ways, spelled out in §2.3 <input> element behavior and §2.4 Opaque FormData objects.
Note: Removing the writeonly
attribute from an element will not clear any write-only value
flags which may have been set. Once that flag is set for an element, it cannot be cleared.
2.1.1. writeonly
IDL
2.1.1.1. HTMLFormElement
partial interface HTMLFormElement { attribute boolean writeonly; };
-
writeonly, of type boolean
-
This IDL attribute reflects the value of the element’s
writeonly
content attribute.
2.1.1.2. HTMLInputElement
partial interface HTMLInputElement { attribute boolean writeonly; };
-
writeonly, of type boolean
-
This IDL attribute reflects the value of the element’s
writeonly
content attribute.
2.1.1.3. HTMLSelectElement
partial interface HTMLSelectElement { attribute boolean writeonly; };
-
writeonly, of type boolean
-
This IDL attribute reflects the value of the element’s
writeonly
content attribute.
2.1.1.4. HTMLTextareaElement
partial interface HTMLTextareaElement { attribute boolean writeonly; };
-
writeonly, of type boolean
-
This IDL attribute reflects the value of the element’s
writeonly
content attribute.
2.2. The form-writeonly
Content Security Policy directive
The form-writeonly
directive specifies a policy which affects how the user agent
interprets elements with autocomplete
attributes in a Document
. The syntax
for the name and value of the directive are described by the following ABNF grammar [ABNF]:
directive-name = "form-writeonly" directive-value = "" / *WSP [ autocomplete-token *[ 1*WSP autocomplete-token ] *WSP ] autocomplete-token = <any valid autofill detail tokens>
Document
's forced write-only types (which has an initial value of the empty list)
as follows:
Given a Document
or global object (context), a response (response), and
a policy (policy):
-
Assert: response is unused.
-
If policy’s dispositionis not "
enforce
", or context is not aDocument
, return. -
If this directive’s value is empty, set context’s forced write-only types to « "
*
" ».Otherwise, set context’s forced write-only types to this directive’s value.
2.3. <input>
element behavior
Expand this to include form
, select
, and textarea
.
If an input
element (input)'s write-only value flag is set, input’s Document
's forced write-only types is « "*
" », or if input’s autocomplete attribute contains one or more values which are also contained in input’s Document
's forced write-only types, then the following restrictions apply:
-
When setting input’s
autocomplete
IDL attribute, input’s write-only value flag MUST also be set.Note: This prevents attackers from bypassing the CSP directive by changing the element’s autocomplete value from
current-password
to something unprotected (likesection-fake
). -
The getters for input’s
value
,valueAsNumber
,valueAsDate
,selectionStart
, andselectionEnd
IDL attributes will throw anInvalidStateError
.Note: Setting these attributes is unaffected.
-
input is considered barred from constraint validation.
-
The user agent MUST NOT fire
keydown
,keyup
, orkeypress
events on input. -
FormData
objects constructed from the form which contains input are opaque, as described in §2.4 Opaque FormData objects
Is this more or less exhaustive? I’m sure I’m missing some clever ways of reading the value.
2.4. Opaque FormData
objects
FormData
objects have a opaque flag, unset by default, and set only
if the object is constructed from a form
containing one or more input
elements whose write-only value flag is set. Opaque FormData
objects return null
and the empty
sequence when their get()
and getAll()
methods are executed,
respectively. Further, data from opaque FormData
objects can only be extracted in the context of executing XMLHttpRequest
's send()
method.
Or fetch()
, presumably? Wonder what I was thinking here...
Opaque FormData
objects have the following properties:
- Whenever the user agent would execute an opaque
FormData
object’sget()
method, it MUST returnnull
. - Whenever the user agent would execute an opaque
FormData
object’sgetAll()
method, it MUST return the empty sequence. -
Whenever the user agent would extract a byte stream and
Content-Type
from an opaqueFormData
object formdata, first the following steps:- If the extraction algorithm is being executed in the context of XMLHttpRequest’s
send()
method, Fetch’sRequest
constructor, or Fetch’sResponse
constructor then skip the next step and proceed executing the algorithm. - Otherwise, abort the extraction algorithm and return the result of
executing the extract a byte stream and
Content-Type
algorithm on a new (empty)FormData
object.
- If the extraction algorithm is being executed in the context of XMLHttpRequest’s
- ISSUE: Determine the right spec text to impart the following: When a request is handed off to Fetch, mark it as opaque if its' body is populated from an opaque FormData object, or if it is the result of a form submission with opaque contents.
2.4.1. Algorithm Modifications
Monkey-patching! Hooray! Though, of course, that’s pretty much this whole strawman...
2.4.1.1. XHR: FormData
constructor
Insert the following steps before step 3 of the FormData
constructor’s algorithm:
- Unset fd’s opaque flag.
-
If form is given, then:
- Let controls be a list of all the submittable elements whose form owner is form.
-
For each element field in controls:
- If field’s write-only value flag is
true
, then set fd’s opaque flag.
- If field’s write-only value flag is
2.4.1.2. XHR:
FormData
’sget()
Redefine
FormData
’sget()
method as follows:- If the opaque flag is set, return
null
. - Otherwise, return the value of the first entry whose name is name, and
null
if no such entry exists.
2.4.1.3. XHR:
FormData
’sgetAll()
Redefine
FormData
’sgetAll()
method as follows:- If the opaque flag is set, return the empty sequence.
- Otherwise, return the values of all entries whose name is name, in list order, and the empty sequence if no such entry exists.
2.4.1.4. Fetch:
Request
objectsAdd a new opaque request flag to Fetch’s
Request
objects. This flag is unset unless otherwise specified.2.4.1.5. Fetch:
Request
’s constructorAdd the following step after step 3 of step 17 of Fetch’s
Request
constructor:- If init’s
body
member is aFormData
object whose opaque flag is set, set r’s opaque flag.
2.4.1.6. Fetch:
Body
’sconsume body
Insert the following step after step 2 of step 3 of
Body
’sas generic
algorithm:- If object’s opaque request flag is set, set stream to an empty byte sequence.
2.4.1.7. Fetch: Extract a byte stream
Redefine the
FormData
case of Fetch’s extract a byte stream andContent-Type
algorithm as follows:-
If object’s opaque flag is not set,
or if the extraction algorithm is being executed in the context of XMLHttpRequest’s
send()
method or Fetch’sRequest
constructorthen:
- Push the result of running the multipart/form-data encoding algorithm, with object as form data set and with utf-8 as the explicit character encoding, to stream.
- Set Content-Type to
multipart/form-data;boundary=
, followed by the multipart/form-data boundary string generated by the multipart/form-data encoding algorithm.
-
Otherwise, set Content-Type to
text/plain;charset=UTF-8
.Note: In this case (e.g. object is opaque and the algorithm isn’t being executed as a result of
XHR.send()
), stream will remain an empty byte stream.
2.4.1.8. Service Worker: Handle a Fetch
Figure out the right way to monkey-patch Service Worker’s Handle a Fetch algorithm to do the right thing with opaque requests.
3. Security and Privacy Considerations
3.1. Sandboxing autofilled credential access
This section is non-normative.
Some user agents are implemented in such a way as to render websites via low-privilege processes that don’t have direct access to the network. Network requests are mediated by a more privileged parent process, which can protect against a number of threats which arise if a clever attacker can exploit bugs in the user agent to corrupt the website’s process.
Even if we prevent DOM-level access to input fields, the form field is somehow represented in memory that’s accessible to that child process. It is quite conceivable that an attacker could exploit browser bugs which corrupt the child process in such a way as to expose a user’s credentials.
Marking fields as write-only might mitigate this risk in the case of autofilled credentials. Since the webpage does not need access to a user’s actual password, the parent process can generate a nonce when autofilling credentials, and ask the child process to fill the password field with that value instead of the actual password. Then, when the child process asks the parent process to POST a form, the parent process can replace occurrences of the nonce in the POST’s body with the actual password for a given credential.
This approach means that the actual credentials are never provided to the high-risk child process, which means that an attacker would have to break out of the child’s sandbox in order to steal credentials.
A more rigorous examination of this approach is undertaken in "Protecting Users Against XSS-based Password Manager Abuse" [STOCK]. Their use of a browser extension is instructive: there’s no reason that this scheme couldn’t be implemented by a third-party credential manager, as long as it has access to the data POSTed from one page to another.
4. Authoring Considerations
This section is non-normative.
In order to mitigate the risk of content injection attacks which steal user credentials, we recommend that authors:
-
Set a Content Security Policy [CSP3] on every page of their sites (including error pages, etc.), and ensure that the policy contains both
form-action
andconnect-src
, as well asform-writeonly
directives. The first will protect against autofilled form submissions which exfiltrate data to an attacker’s origin, the second against XHR-based submissions, and the last against DOM-level content injection attacks. -
Always place sign-in forms on TLS-protected pages, and always submit such forms to TLS-protected endpoints. If credentials are sent in the clear, they are trivially vulnerable to theft.
5. Acknowledgements
This proposal is heavily inspired by Jacob S Hoffman-Andrews' comments on public-webapps@, as well as Brian Smith’s response. Anne van Kesteren helped immensely with the Fetch integration.