Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

One possible way to handle the nested iframe case. #5

Merged
merged 9 commits into from
Mar 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,21 @@ In this example,

The characters `"@"` and `"#"` identify the origin and the code, respectively. (We can always introduce additional prefix characters in the future if it turns out we need to include additional information in these messages.)

Some sites use third-party `iframe`s for authentication. In such cases, the third-party `iframe`'s origin can be specified using the same `"@"` field after the code.

```text
747723 is your ExampleCo authentication code.

@example.com #747723 @ecommerce.example
```

In this example,

* `"https://example.com"` is the origin the code is associated with,
* `"747723"` is the code,
* `"https://ecommerce.example"` is the origin of the third-party `iframe`, and
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The attack vector I'm concerned about here is something along the lines of an ad network running on an iframe and sending SMSes to users to identify them. There is user permission involved, which I think helps, but still feels more dangerous than needed.

Can you help me understand what are the alternatives you considered?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not sure I’m seeing the attack. The iframe extension only lets the party sending the text bind the code to an iframe they trust on their own page. Can you be more specific about the attack vector here?

The alternatives considered were:

  1. Implicitly allow a code bound to a website to be used on all subframes of the website. We considered this to be a potential security risk.
  2. Never allow use of origin-bound codes in iframes. We found this to be limiting, because some real-world websites delegate authentication to use auth widgets from other origins.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The attack vector I have in mind here is that there are a lot of widgets that are embedded on websites as iframes at scale. Ad networks, analytics, commenting widgets, etc are highly deployed.

  1. highly-deployed iframe-widget evil.com wants to find a user on the web on example.com
  2. example.com embeds evil.com as an iframe
  3. user goes to example.com
  4. evil.com suspects a specific user of interest is at example.com and sends an SMS to the user's phone
  5. browser shares the SMS with evil.com while the user is on example.com
  6. evil.com now gets confirmation that the specific user was visiting example.com

This, I think, is mitigated if the user agent asks for the user's consent on step (5), which I think is sufficiently true for the autocomplete="one-time-code" formulation which uses autocomplete and the current formulation of WebOTP, but seems like a hard requirement going forward for future implementations of WebOTP (at least for iframe-based use cases it would seem). I'm wondering if this requirement needs to be encapsulated here in this spec or on the WebOTP spec specifically.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other specifications have resolved this class of issue through Feature Policy. If usage of WebOTP in cross-origin frames required opt-in from the top-level origin then I think that would resolve the concern in @rmondello's alternative (1).

* `"747723 is your ExampleCo authentication code.\n\n"` is human-readable explanatory text.

## Benefits

Adoption of this format would improve the reliability of systems which today heuristically extract one-time codes from SMS, with clear end-user benefit. It improves reliability of both extracting the code and also associating that code with an origin.
Expand Down
114 changes: 84 additions & 30 deletions index.bs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ Complain About: accidental-2119 true
<pre class="link-defaults">
spec:infra; type:dfn; text:size; for:list
spec:infra; type:dfn; text:string
spec:url; type:dfn; text:origin
spec:url; type:dfn; text:scheme
</pre>

Expand All @@ -30,7 +29,7 @@ Many websites deliver one-time codes over SMS. [[GSM-SMS]]

Without a standard format for such messages, programmatic extraction of codes from them has to rely on heuristics, which are often unreliable and error-prone. Additionally, without a mechanism for associating such codes with specific websites, users might be tricked into providing the code to malicious sites.

This specification defines a format for the delivery of one-time codes over SMS. This format associates the one-time code with a specific [=origin=].
This specification defines a format for the delivery of one-time codes over SMS. This format associates the one-time code with a specific website.

</div>

Expand All @@ -40,31 +39,58 @@ This specification depends on the Infra Standard. [[!INFRA]]

<h2 id="origin-bound-one-time-codes">Origin-bound one-time codes</h2>

An <dfn export>origin-bound one-time code</dfn> is a [=tuple=] consisting of an [=origin=] and a code (a [=string=]).
An <dfn export>origin-bound one-time code</dfn> is a [=tuple=] consisting of a top-level origin (an [=/origin=]), an embedded origin (an [=/origin=] or `null`), and a code (a [=string=]).

<div class=example>

((`"https"`, `"example.com"`, `null`, `null`), `"747723"`) is an [=origin-bound one-time code=] whose origin is (`"https"`, `"example.com"`, `null`, `null`) and whose code is `"747723"`.
((`"https"`, `"example.com"`, `null`, `null`), `null`, `"747723"`) is an [=origin-bound one-time code=] whose top-level origin is (`"https"`, `"example.com"`, `null`, `null`), whose embedded origin is `null`, and whose code is `"747723"`.

</div>

<h3 id="usage">Usage</h3>

Many User Agents help users fill out forms on websites. Sites can use features like <a href="https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete-one-time-code">`autocomplete=one-time-code`</a> to hint to User Agents that they could assist the user with providing a one-time code to the website. [[HTML]]
<div class=example>

<!-- We should be able to reference autocomplete=one-time-code with Bikeshed syntax along the lines of <{html/autocomplete/one-time-code}>. See whatwg/html#5418. -->
((`"https"`, `"example.com"`, `null`, `null`), (`"https"`, `"ecommerce.example"`, `null`, `null`), `"747723"`) is an [=origin-bound one-time code=] whose origin is (`"https"`, `"example.com"`, `null`, `null`), whose embedded origin is (`"https"`, `"ecommerce.example"`, `null`, `null`), and whose code is `"747723"`.

In this section, an <dfn>active origin</dfn> is an [=origin=] of a [=top-level browsing context=]'s [=active document=].
</div>

When a User Agent is in possession of an [=origin-bound one-time code=] and an [=active origin=] is <strong>[=same origin=]</strong> with the [=origin-bound one-time code=]'s origin, the User Agent may assist the user with providing the [=origin-bound one-time code=]'s code to the website.
<h3 id="usage">Usage</h3>

When a User Agent is in possession of an [=origin-bound one-time code=] and an [=active origin=] is <strong>[=same site=] but not [=same origin=]</strong> with the [=origin-bound one-time code=]'s origin, the User Agent may assist the user with providing the [=origin-bound one-time code=]'s code to the website, and should indicate the [=origin-bound one-time code=]'s origin to the user.
Many User Agents help users fill out forms on websites. Sites can use features like <a href="https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete-one-time-code">`autocomplete=one-time-code`</a> to hint to User Agents that they could assist the user with providing a one-time code to the website. [[HTML]]

When a User Agent is in possession of an [=origin-bound one-time code=] and an [=active origin=] is <strong>neither [=same site=] nor [=same origin=]</strong> with the [=origin-bound one-time code=]'s origin, the User Agent should not assist the user with providing the [=origin-bound one-time code=]'s code to the website.
Note: This specification does not impose any requirements or restrictions on the use of one-time codes which are not [=origin-bound one-time codes=].

Note: because the [=scheme=] of an [=origin-bound one-time code=]'s origin is always `"https"`, assisting the user with providing [=origin-bound one-time codes=] is only available in [=secure contexts=].
<!-- We should be able to reference autocomplete=one-time-code with Bikeshed syntax along the lines of <{html/autocomplete/one-time-code}>. See whatwg/html#5418. -->

This specification does not impose any requirements or restrictions on the use of one-time codes which are not [=origin-bound one-time codes=].
User Agents determine whether or not to assist the user to provide an origin-bound one-time code to a website with [=origin-bound one-time code=] |otc| and {{Document}} |doc| by running these steps:

1. If |doc| is not the [=active document=] of a [=/browsing context=], return failure.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here: wondering if you are expecting this repo to specify browser API behavior or specifically the formatting of the SMS.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same answer.

1. Let |context| be |doc|'s [=Document/browsing context=].
1. If |context| is a [=top-level browsing context=]. run these steps:
1. If |otc|'s embedded origin is not `null`, return failure.
1. If |otc|'s top-level origin is [=same origin=] with |doc|'s [=Document/origin=], return `"origin"`.
1. If |otc|'s top-level origin is [=same site=] with |doc|'s [=Document/origin=], return `"site"`.
1. Return failure.
1. If |otc|'s embedded origin is `null`, return failure.
1. Let |match type| be `"origin"`.
1. If |otc|'s embedded origin is not [=same origin=] with |doc|'s [=Document/origin=], set |match type| to `"site"`.
1. If |otc|'s embedded origin is not [=same site=] with |doc|'s [=Document/origin=], return failure.
1. Set |context| to its [=parent browsing context=].
1. While |context| is not a [=top-level browsing context=], run these steps:
1. If |context|'s [=active document=]'s [=Document/origin=] is [=same origin=] with neither |otc|'s embedded origin nor |otc|'s top-level origin, set |match type| to `"site"`.
1. If |context|'s [=active document=]'s [=Document/origin=] is [=same site=] with neither |otc|'s embedded origin nor |otc|'s top-level origin, return failure.
1. Set |context| to its [=parent browsing context=].
1. If |context| is not a [=top-level browsing context=], return failure.
1. If |context|'s [=active document=]'s [=Document/origin=] is [=same origin=] with |otc|'s top-level origin, return |match type|.
1. If |context|'s [=active document=]'s [=Document/origin=] is [=same site=] with |otc|'s top-level origin, return `"site"`.
1. Return failure.

If the above steps returned `"origin"` or `"site"`, the User Agent may assist the user with providing the [=origin-bound one-time code=]'s code to the website.

If the above steps returned `"site"`, the User Agent should indicate the [=origin-bound one-time code=]'s top-level and embedded origins to the user when assisting them.

If the above steps returned failure, the User Agent should not assist the user with providing the [=origin-bound one-time code=]'s code to the website.

Note: because the [=schemes=] of an [=origin-bound one-time code=]'s top-level and embedded origins are always `"https"`, assisting the user with providing [=origin-bound one-time codes=] is only available in [=secure contexts=].

<h2 id="format">Message format</h2>

Expand All @@ -77,11 +103,11 @@ An <dfn export>origin-bound one-time code message</dfn> is a [=string=] for whic

<em>This section is non-normative. [[#parsing]] is the normative text.</em>

[=Origin-bound one-time code messages=] can optionally begin with human-readable <dfn for="origin-bound one-time code message">explanatory text</dfn>. This consists of all but the last line of the message. The last line of the message contains both a <dfn for="origin-bound one-time code message">host</dfn> and a <dfn for="origin-bound one-time code message">code</dfn>, each prefixed with a sigil: U+0040 (@) before the <a for="origin-bound one-time code message">host</a>, and U+0023 (#) before the [=code=].
[=Origin-bound one-time code messages=] can optionally begin with human-readable <dfn for="origin-bound one-time code message">explanatory text</dfn>. This consists of all but the last line of the message. The last line of the message contains both a <dfn for="origin-bound one-time code message">top-level host</dfn> and a <dfn for="origin-bound one-time code message">code</dfn>, each prefixed with a sigil: U+0040 (@) before the [=origin-bound one-time code message/top-level host=], and U+0023 (#) before the [=code=]. Following the [=code=], an <dfn for="origin-bound one-time code message">embedded host</dfn> can be specified. It is preceeded with a U+0040 (@) sigil.

<div class="example">

In the following [=origin-bound one-time code message=], the <a for="origin-bound one-time code message">host</a> is `"example.com"`, the [=code=] is `"747723"`, and the [=explanatory text=] is `"747723 is your ExampleCo authentication code.\n\n"`.
In the following [=origin-bound one-time code message=], the [=origin-bound one-time code message/top-level host=] is `"example.com"`, the [=code=] is `"747723"`, no [=origin-bound one-time code message/embedded host=] is specified, and the [=explanatory text=] is `"747723 is your ExampleCo authentication code.\n\n"`.

```
"747723 is your ExampleCo authentication code.
Expand All @@ -91,21 +117,33 @@ In the following [=origin-bound one-time code message=], the <a for="origin-boun

</div>

The last line has to begin with U+0040 (@). (Which is to say, the <a for="origin-bound one-time code message">host</a> always comes before the [=code=] in the message.)
<div class="example">

In the following [=origin-bound one-time code message=], the [=origin-bound one-time code message/top-level host=] is `"example.com"`, the [=code=] is `"747723"`, the [=origin-bound one-time code message/embedded host=] is `"ecommerce.example"`, and the [=explanatory text=] is `"747723 is your ExampleCo authentication code.\n\n"`.

```
"747723 is your ExampleCo authentication code.

@example.com #747723 @ecommerce.example"
```

</div>

The order of fields in the last line is always [=origin-bound one-time code message/top-level host=], [=code=], and [=origin-bound one-time code message/embedded host=] (if present). Nothing can come before the [=origin-bound one-time code message/top-level host=] in the last line.

<div class="example">

The message `"something @example.com #747723"` is not an [=origin-bound one-time code message=], because its last line does not begin with U+0040 (@).
The message `"something @example.com #747723"` is not an [=origin-bound one-time code message=], because it doesn't start with the [=origin-bound one-time code message/top-level host=].

</div>

<div class="example">

The message `"#747723 @example.com"` is not an [=origin-bound one-time code message=], because its last line does not begin with U+0040 (@).
The message `"#747723 @ecommerce.example @example.com"` is not an [=origin-bound one-time code message=], because the fields are in the wrong order.

</div>

Exactly one U+0020 (SPACE) separates the two values in the last line of the message.
Exactly one U+0020 (SPACE) separates the values in the last line of the message.

<div class="example">

Expand All @@ -117,7 +155,7 @@ Trailing text in the last line is ignored. This is because we might identify add

<div class="example">

In the [=origin-bound one-time code message=] `"@example.com #747723 %future"`, the <a for="origin-bound one-time code message">host</a> is `"example.com"`, the [=code=] is `"747723"`, and the [=explanatory text=] is `""`. The trailing text `" %future"` is ignored.
In the [=origin-bound one-time code message=] `"@example.com #747723 @ecommerce.example $future"`, the [=origin-bound one-time code message/top-level host=] is `"example.com"`, the [=code=] is `"747723"`, the [=origin-bound one-time code message/embedded host=] is `"ecommerce.example"`, and the [=explanatory text=] is `""`. The trailing text `" %future"` is ignored.

</div>

Expand All @@ -128,17 +166,32 @@ In the [=origin-bound one-time code message=] `"@example.com #747723 %future"`,
To <dfn export type="abstract-op">parse an origin-bound one-time code message</dfn> from |message|, run these steps:

1. Let |line| be the [=last line=] of |message|, and |position| be 0.
1. If the code point at |position| within |line| is not U+0040 (@), return failure.
1. Advance |position| by 1.
1. Let |host| be the result of [=collecting a sequence of code points=] which are not [=ASCII whitespace=] from |line| with |position|.
1. If |host| is the empty string, return failure.
1. If the code point at |position| within |line| is not U+0020 (SPACE), return failure.
1. If |position| points past the end of |line|, return failure.
1. Let |top-level host| be the result of [=extract a marked token|extracting a marked token=] from |line| at |position| with marker U+0040 (@).
1. If |top-level host| is failure, return failure.
1. Let |top-level origin| be the [=/origin=] (`"https"`, |top-level host|, `null`, `null`).
1. If |position| points past the end of |line|, return failure.
1. If the [=code point=] at |position| within |line| is not U+0020 (SPACE), return failure.
1. Advance |position| by 1.
1. If the code point at |position| within |line| is not U+0023 (#), return failure.
1. If |position| points past the end of |line|, return failure.
1. Let |code| be the result of [=extract a marked token|extracting a marked token=] from |line| at |position| with marker U+0023 (#).
1. If |code| is failure, return failure.
1. Let |embedded origin| be null.
1. If |position| does not point past the end of |line|, and if the [=code point=] at |position| within |line| is U+0020 (SPACE), run the following steps:
1. Advance |position| by 1.
1. Let |embedded host| be the result of [=extract a marked token|extracting a marked token=] from |line| at |position| with marker U+0040 (@).
1. If |embedded host| is failure, set |embedded origin| to null. Otherwise, set |embedded origin| to the [=/origin=] (`"https"`, |embedded host|, `null`, `null`).
1. Return the [=origin-bound one-time code=] (|top-level origin|, |embedded origin|, |code|).

To <dfn type="abstract-op">extract a marked token</dfn> from |string| at |position| with [=code point=] |marker|, run the following steps:

1. If |position| points past the end of |string|, return failure.
1. If the [=code point=] at |position| within |string| is not |marker|, return failure.
1. Advance |position| by 1.
1. Let |code| be the result of [=collecting a sequence of code points=] which are not [=ASCII whitespace=] from |line| with |position|.
1. If |code| is the empty string, return failure.
1. Return the [=origin-bound one-time code=] ((`"https"`, |host|, `null`, `null`), |code|).
1. If |position| points past the end of |string|, return failure.
1. Let |token| be the result of [=collecting a sequence of code points=] which are not [=ASCII whitespace=] from |string| with |position|.
1. If |token| is the empty string, return failure.
1. Return |token|.

The <dfn type=abstract-op>last line</dfn> of |string| is the result of running these steps:

Expand All @@ -164,6 +217,7 @@ On some platforms, User Agents might need access to all incoming SMS messages—

Many thanks to
Aaron Parecki,
Elaine Knight,
Eric Shepherd,
Eryn Wells,
Jay Mulani,
Expand Down