Re: [OAUTH-WG] WGLC for Browser-based Apps

Yannick Majoros <yannick@valuya.be> Tue, 29 August 2023 01:03 UTC

Return-Path: <yannick@valuya.be>
X-Original-To: oauth@ietfa.amsl.com
Delivered-To: oauth@ietfa.amsl.com
Received: from localhost (localhost [127.0.0.1]) by ietfa.amsl.com (Postfix) with ESMTP id CAF5AC151532 for <oauth@ietfa.amsl.com>; Mon, 28 Aug 2023 18:03:28 -0700 (PDT)
X-Virus-Scanned: amavisd-new at amsl.com
X-Spam-Flag: NO
X-Spam-Score: -2.094
X-Spam-Level:
X-Spam-Status: No, score=-2.094 tagged_above=-999 required=5 tests=[BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, HTML_MESSAGE=0.001, RCVD_IN_ZEN_BLOCKED_OPENDNS=0.001, SPF_HELO_NONE=0.001, T_SCC_BODY_TEXT_LINE=-0.01, T_SPF_PERMERROR=0.01, URIBL_BLOCKED=0.001, URIBL_DBL_BLOCKED_OPENDNS=0.001, URIBL_ZEN_BLOCKED_OPENDNS=0.001] autolearn=ham autolearn_force=no
Authentication-Results: ietfa.amsl.com (amavisd-new); dkim=pass (1024-bit key) header.d=valuya.be
Received: from mail.ietf.org ([50.223.129.194]) by localhost (ietfa.amsl.com [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id hvjm0eqyl8PW for <oauth@ietfa.amsl.com>; Mon, 28 Aug 2023 18:03:23 -0700 (PDT)
Received: from mail-ua1-x92e.google.com (mail-ua1-x92e.google.com [IPv6:2607:f8b0:4864:20::92e]) (using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by ietfa.amsl.com (Postfix) with ESMTPS id CF4D3C15109B for <oauth@ietf.org>; Mon, 28 Aug 2023 18:03:23 -0700 (PDT)
Received: by mail-ua1-x92e.google.com with SMTP id a1e0cc1a2514c-7a50bd29064so16488241.3 for <oauth@ietf.org>; Mon, 28 Aug 2023 18:03:23 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=valuya.be; s=google; t=1693271002; x=1693875802; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:from:to:cc:subject:date:message-id:reply-to; bh=kVZN3iS6j95DuuGksKvB93AThf7LuNE1PjMqU4vj9XA=; b=kEaz6Hop+vVQ4jk3lhPzE88EO9i1PJeqkB9KNIzlroxpl1Zs5Dwg0jzQ5GZakjqVbc 1AyLb36HfQ+0bU05Tsn+E1U4ulWP4Lu6nsi78j3zvkSyGtzQV80Nl89UVdZovakhcIli /6uEepxkOiu9krfN/jm3Bw/SXjvIyEvRXAULU=
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20221208; t=1693271002; x=1693875802; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=kVZN3iS6j95DuuGksKvB93AThf7LuNE1PjMqU4vj9XA=; b=HU5dzsfAaGyqaYXdqq6FxAQBW85SZFyjH8m+sPeari3W1sGiloWsBJkvL86ULLOFyv lEYEtE8eB5g3krQHu1avz9oq1S8KJIUeNBZlSFS5/b6NRuHQO+ZHPkeoB2o0fKizqpBt JiA53O404JkBDTW2QWE35WGOsnwP2iAPr5dMz9JJQOUZ3eDxc3GAbAx0lv8KFmFJTk79 6Ur558P/zeEr/nQtiTMXZfwEZDb8Nby1Ij4tJrubRQvE1nNIfnRC3Hl56goE/q9bzTjr 6nnPUJHZt/qTWkt/vLLys3aO0jMIn+uvZR9YVGO4DjL5Bx3VgbqSl8C6WxcK9MA3jOo7 nV8g==
X-Gm-Message-State: AOJu0YwC1cMBYHSDYePkG+vs0ThM6i2K3ia08QVZpyrvWJ16bQ2llm8U 5QIYH5s2i2+fGex9OGlkokPwBm15ipCaE77gIN7zmbkyCK9jWv60+YUMDw==
X-Google-Smtp-Source: AGHT+IF/qZIB8jh9QdhxbIciEK+Ol5/x88hbx9Wka8WZBti05A0agFc0eMkaH76bXZbFfJcHBWCJgiSs1tZR8XC7k5c=
X-Received: by 2002:a05:6102:3e16:b0:44e:c2d6:cbcf with SMTP id j22-20020a0561023e1600b0044ec2d6cbcfmr3126065vsv.8.1693271001745; Mon, 28 Aug 2023 18:03:21 -0700 (PDT)
MIME-Version: 1.0
References: <7E70458F-85C3-4C6B-8685-61B58E12E28D@pragmaticwebsecurity.com> <66996CD0-48EA-464D-B1B8-E6968B465FD7@manicode.com> <CALNQ_jLYmAePAsqf1=u_4vGLuSGUdSgkPzSgz75CQRJuSfv7qw@mail.gmail.com> <5EC8605B-CF11-4217-9B56-2D459F944C6D@pragmaticwebsecurity.com>
In-Reply-To: <5EC8605B-CF11-4217-9B56-2D459F944C6D@pragmaticwebsecurity.com>
From: Yannick Majoros <yannick@valuya.be>
Date: Tue, 29 Aug 2023 03:03:10 +0200
Message-ID: <CALNQ_jLg3W1JMFWpS_Ge2cCfnF5ZmUjQUs=cn4-HA9cHUoEmPg@mail.gmail.com>
To: Philippe De Ryck <philippe@pragmaticwebsecurity.com>
Cc: Jim Manico <jim@manicode.com>, oauth <oauth@ietf.org>
Content-Type: multipart/alternative; boundary="000000000000915b44060405607a"
Archived-At: <https://mailarchive.ietf.org/arch/msg/oauth/yhMiAJgEIOCoPY5FD0D28_ByqFs>
Subject: Re: [OAUTH-WG] WGLC for Browser-based Apps
X-BeenThere: oauth@ietf.org
X-Mailman-Version: 2.1.39
Precedence: list
List-Id: OAUTH WG <oauth.ietf.org>
List-Unsubscribe: <https://www.ietf.org/mailman/options/oauth>, <mailto:oauth-request@ietf.org?subject=unsubscribe>
List-Archive: <https://mailarchive.ietf.org/arch/browse/oauth/>
List-Post: <mailto:oauth@ietf.org>
List-Help: <mailto:oauth-request@ietf.org?subject=help>
List-Subscribe: <https://www.ietf.org/mailman/listinfo/oauth>, <mailto:oauth-request@ietf.org?subject=subscribe>
X-List-Received-Date: Tue, 29 Aug 2023 01:03:28 -0000

Hello Philippe,

Thanks for the new details. This new information let me indeed reproduce
the exploit, which seems different from the January one, that I wasn't
able to successfully reproduce against my current implementation.

*> For someone who is more than eager to demand that we prove them wrong by
showing a fully-detailed implementation and exploit, you seem to be very
unwilling to spend any of your own time trying to understand the arguments
that have been provided. The only arguments you have provided so far boil
down to “trust me, you’re wrong”, even when faced with a clear
demonstration. Furthermore, your description of why the SW should work are
based on a severe lack of understanding of how browser-based security works*
.
*> The process of dropping a proposal that you do not use in practice, and
then demanding that we keep convincing you over and over again that it is a
severely flawed proposal shows an enormous lack of respect. I have shown
you the courtesy of trying to convince you, but since it’s clear that you
are unwilling to even critically inspect your own idea, it’s time to wrap
this up. *

Could we return to a more objective and constructive discussion? I've
invested a significant amount of my own time into your emails, and even
more into the subject itself. My time holds the same value as yours, and I
believe it's essential to treat each other's time with respect. My primary
goal is centered around developing secure web applications and documenting
various approaches. I can be wrong, I can miss details, but it remains
important for this document to be based on proven facts and to demonstrate
any claims made. Criticisms like 'a lack of understanding of browser-based
security' are counterproductive and do not contribute to productive
conversations. I did not drop a subject or even call it a best practice: I
started documenting an alternative approach and now acknowledge its current
imperfections, as was recently highlighted by you, though I wasn't able to
reproduce any exploit from the previous discussions (and this document has
been consistently reviewed over the course of a year, without receiving
substantial public feedback). In an attempt to shed light on specifics and
to challenge the approach effectively, I proposed creating a test
implementation within this very email thread. Thus, statements made here,
such as a lack of  personal investment to 'understand your arguments,' are
not accurate. A proof of concept serves as one way towards understanding. I
still think it would be useful, though implementation was initially
regarded as out of scope, as the unregistration problem seems fixable, for
example. I've also noticed instances where I had to reiterate certain
points without seemingly being acknowledged. To conclude this digression, I
apologize if any of my statements or inquiries came across as
disrespectful, as that was never my intention. I anticipate and hope for an
equitable and inclusive dialogue moving forward.

Though I don't use the pattern personally anymore (all my clients switched
to local storage and got rid of refresh tokens), I did for some time and I
still think it has value. When doing some research for this thread, I
noticed it's being incrementally used (just one example here:
https://github.com/infinum/auth-worker), so there is definitely some
interest. I do think further research is needed in its current state.

Best regards,

Yannick

Le lun. 28 août 2023 à 18:40, Philippe De Ryck <
philippe@pragmaticwebsecurity.com> a écrit :

>
>
> Again, there is something fundamentally misunderstood here: Philippe's
> exploit will not work with a correctly implemented service worker. Also not
> in an iframe. Also not if you unregister it and you start a new iframe.
>
>
> For someone who is more than eager to demand that we prove them wrong by
> showing a fully-detailed implementation and exploit, you seem to be very
> unwilling to spend any of your own time trying to understand the arguments
> that have been provided. The only arguments you have provided so far boil
> down to “trust me, you’re wrong”, even when faced with a clear
> demonstration. Furthermore, your description of why the SW should work are
> based on a severe lack of understanding of how browser-based security
> works.
>
> I cannot spend any more time on discussing a flawed and theoretical
> solution, of which you have yourself admitted that you do not use it in
> practice. However, in the interest of educational purposes, I am willing to
> use this last message in this discussion to outline the flaws in this
> solution. I trust you find this approach pragmatic enough.
>
>
> There is no "need to explain yourself several times" and nobody has
> "already demonstrated back in January that this approach is not effective",
> because a correctly implemented service worker can effectively prevent this
> attack. The attacker cannot "run a new flow" and automate getting a token
> if the service worker implementation follows the specified guidelines:
> *§6.4.2.1*
>
> *    * The application MUST register the Service Worker before running any
> code interacting with the user.*
>
> The redirect URI is registered within your application. If you allow any
> redirect_uri to be a silent refresh flow (typically in an iframe), you
> *must* make it synchronously register the service worker as the first
> action on the page, just as any other part of the application (and stopping
> that is a whole different attack than XSS). The single-threaded nature of
> javascript and synchronous loading of scripts will not let any secret leak
> before it has been loaded (and as an additional counter-measure for
> half-manual access code leaks, you could add some restriction to remove
> auth codes from pages before it is loaded).
>
>
> This paragraph clearly illustrates that you still do not grasp the attack
> scenario, which kind of undermines the “trust me bro” arguments. Let me
> walk you through the scenario in a chronological order. And to be 100%
> clear about why things are the way they are, I added source references in
> red.
>
>
>    1. The application on example.com loads in the main browsing context
>    (referred to as APP from now on) and registers a SW [Based on your
>    proposal in the spec and the image in your GH demo repo [1]]
>    2. The SW runs the authorization code flow, obtains tokens, and does
>    not expose anything to the APP  [Based on your proposal in the spec
>    and the image in your GH demo repo [1]]
>    3. The APP can make API calls, which are intercepted the SW and
>    augmented with tokens before they go out  [Based on your proposal in
>    the spec and the image in your GH demo repo [1]]
>    4. The attacker triggers the ability to execute malicious JS (e.g.,
>    XSS)
>    5. The malicious JS unregisters the SW. Because of this
>    unregistration, the SW will not be used for a new browsing context (e.g.,
>    an iframe). The SW will however be re-registered when there’s a page load
>    from example.com, which does not happen at this point. [Based on the
>    considerations in the spec:
>    https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps#section-6.4.2.2
>    ]
>    6. The attacker sets up a piece of malicious JS code (ATTACK) that
>    runs every 1ms (we’ll come back to this)
>    7. The attacker creates a new iframe (FRAME), inserts it into the
>    page, and points the FRAME to the authorization endpoint of the AS to run a
>    silent authorization code flow. The SW is *not present* to intercept
>    this call, so it will reach the AS. [The fact that this step is
>    possible is demonstrated in the recording from January (
>    https://youtu.be/OpFN6gmct8c?feature=shared&t=1973), albeit in a more
>    rudimentary fashion]
>    8. The AS receives a request with cookies, and issues an authorization
>    code in response. This code is delivered through a redirect, so the
>    response carries a Location header with value
>    https://example.com/callback?code=ABC123 [As defined by the OAuth
>    specs. If the response_mode=web_message value is supported, no redirect
>    happens and the attacker receives a message containing the code, as
>    demonstrated in the recording from January.]
>    9. Because of the Location header, the browser will try to load the
>    callback from APP in the FRAME. *However, the ATTACK code will prevent
>    this, as described below*. [Demonstrated at OSW2023, code snippet
>    included below]
>       - The ATTACK code is monitoring the URL of the iframe, which throws
>       an error for cross-origin frames, but works just fine once the FRAME
>       becomes same-origin, hence the aggressive timer.
>       - The moment the URL becomes same-origin, it becomes readable, and
>       the ATTACK code receives a string value that looks like this:
>       https://example.com/callback?code=ABC123 (you can see it coming,
>       right?)
>       - Next, the attacker stops the frame from loading. This means that
>       the browser will either not send a request to
>       https://example.com/callback?code=ABC123 OR the browser will abort
>       the ongoing request. *Either way, it has two very important
>       consequences: (1) the page from APP does not load, and no new SW will be
>       registered, (2) the page from APP does not load, and the authorization code
>       will not be exchanged by APP*
>       - The attacker is now free to extract the authorization code and
>       exchange it for tokens
>
>
> I have demonstrated step 9 at OSW2023. The code for doing this is
> ridiculously simple, which further highlights the dangerous false sense of
> security that this approach presents. For completeness, you can find the
> demo code I used below. Note that this is a quick and dirty PoC, which can
> certainly be improved.
>
>
> function stealCode(frame) {
> let counter = 0;
> function doTheStealing() {
> try {
> const url = frame.contentWindow.location.href
> if(url.includes("code")) {
> frame.contentWindow.stop()
> clearInterval(interval)
>
> const code = url.split("code=")[1].split("&")[0]
> forwardAuthorizationCode(code)
> }
> // Fallback to avoid eternal loops
> if(counter++ > 20000) {
> clearInterval(interval)
> }
> }
> catch(e) {} // Ignore errors when the frame is cross-origin
> }
>
> const interval = setInterval(doTheStealing, 1)
> }
>
>
> To summarize, this chronological story illustrates that the
> single-threaded nature of JS is absolutely irrelevant. It also shows that
> “preventing secrets from being leaked” sounds cool, but is a non-sensical
> statement. After all, how would a SW prevent the attacker’s code running in
> APP to inspect the URL of FRAME?
>
>
> Hence my proposal: instead of a demonstration where you test a
> possibly incomplete implementation (which, as far as I can see, doesn't
> have the fine details that make it fool-proof), I propose to deliver a
> proof-of-contest that would follow these guidelines. Before admitting that
> "you cannot secure browser-flows only", I'd still want to actually see that
> you can do this (which isn't the case from the explanation I read this
> far). This whole story can perfectly be all wrong, but it's work checking
> first. Let's be pragmatic, right?
>
>
> The process of dropping a proposal that you do not use in practice, and
> then demanding that we keep convincing you over and over again that it is a
> severely flawed proposal shows an enormous lack of respect. I have shown
> you the courtesy of trying to convince you, but since it’s clear that you
> are unwilling to even critically inspect your own idea, it’s time to wrap
> this up.
>
> I hope that the rest of the community at least values my contributions.
> Note that I am in fact working with Aaron to ensure that the browser-based
> apps BCP accurately reflects the security properties of the various
> approaches.
>
> Kind regards
>
> Philippe
>
> [1]
> https://github.com/Valuya/servicewauther/blob/e3e4a3db5a77b272380ad7c44547ae842fc719a1/documentation/serviceworker_sequence.png
>
> —
> *Pragmatic Web Security*
> *Security for developers*
> https://pragmaticwebsecurity.com/
>
>
>
>
> Le lun. 28 août 2023 à 14:15, Jim Manico <jim@manicode.com> a écrit :
>
>> *applause*
>>
>> Sucks you need to explain yourself several times but this is very helpful
>> for the community.
>>
>> On Aug 28, 2023, at 7:52 AM, Philippe De Ryck <
>> philippe@pragmaticwebsecurity.com> wrote:
>>
>> Responses inline.
>>
>> Still, there is some initial incorrect point that makes the rest of the
>> discussion complicated, and partly wrong.
>>
>>
>> I believe the key to make the discussion less complicated is to
>> acknowledge that there are two separate issues:
>>
>> 1. An attacker can potentially obtain tokens from the legitimate
>> application
>> 2. An attacker can obtain a set of tokens from the AS directly,
>> completely independent of any application behavior
>>
>> Given that the goal is to prevent an attacker from obtaining tokens,
>> scenario 1 becomes irrelevant when scenario 2 is a possibility. It would be
>> really helpful to analyze the SW approach with this in mind. I’ll add
>> comments inline to highlight why this matters.
>>
>>
>> Specifically, §6.4.2.1 says this: *The service worker MUST NOT transmit
>> tokens, authorization codes or PKCE code verifier to the frontend
>> application.*
>>
>> Wording should be refined, but the idea is that the service worker is
>> to actually restrict authorization codes from even reaching the frontend.
>> Of course, easier said than done, but that part happens to be quite easy to
>> implement.
>>
>>
>> This is related to both scenarios. If the SW is running, you can indeed
>> hide tokens from the main browsing context, which helps to support scenario
>> 1. For scenario 2, you need the guarantee that the SW will intercept *all new
>> flows*, otherwise the attacker  can run a silent flow. *As long as the
>> SW is running* in the main context, I would assume that the attacker can
>> indeed not reach the authorization endpoint directly.
>>
>> The key part above is “as long as the SW is running”. An attacker with
>> the ability to run malicious JS can *unregister* the SW that prevents
>> the attacker from reaching the authorization endpoint.
>>
>> I have raised this issue before, and the response back then was that the
>> SW is only actually removed after the browsing context reloads, which is
>> true. So from the main context, the attacker cannot launch the attack.
>> However, when the attacker instantiates a new browsing context (i.e., an
>> iframe), the unregistered SW is no longer present, and is thereby not able
>> to restrict access to the authorization endpoint.
>>
>> I address this concern in the talk I have referenced before. This link
>> with the time code included (
>> https://youtu.be/OpFN6gmct8c?feature=shared&t=1973) points you to the
>> exact demo scenario, where I illustrate how an unregistered SW cannot
>> prevent access to an endpoint in an iframe. Admittedly, I have not
>> implemented a full OAuth client as a SW, but the minimal PoC you see here
>> suffices to illustrate the ineffectiveness of this approach.
>>
>> With this information, the attack scenario becomes the following:
>>
>>    1. The attacker unregisters the SW in the main browsing context,
>>    preventing it from being used in any new browsing context
>>    2. The attacker injects a new iframe and points it to the
>>    authorization endpoint
>>    3. The AS responds with a redirect with the authorization code
>>    4. The attacker detects the redirect, copies the authorization code,
>>    and aborts the page from loading (so that the authorization code is never
>>    exchanged or the SW is never reloaded)
>>    5. The attacker extracts the authorization code and exchanges it for
>>    tokens
>>
>>
>>
>> TL;DR: a SW is not a security mechanism, and the browser cannot guarantee
>> that a SW permanently prevents requests to a certain endpoint.
>>
>>
>> This has further impact on much of the other statements:
>> *> The main problem with a browser-only client is that the attacker with
>> control over the client has the ability to run a silent Authorization Code
>> flow, which provides them with an independent set of tokens*
>> [...]
>> *> **The security differences between a BFF and a browser-only app are
>> not about token storage, but about the attacker being able to run a new
>> flow to obtain tokens.*
>> [...]
>> *> Again, the security benefits of a BFF are not about stoken storage.
>> Even if you find the perfect storage solution for non-extractable tokens in
>> the browser, an attacker still controls the client application and can
>> simply request a new set of tokens. *
>>
>> Truth is: no, you can't start a new authentication flow and get the
>> authorization code back in the main thread. I'm talking about the
>> redirection scenario, which I'm the most familiar with, but it would
>> probably apply to the "message" one as well (which is new to me and seems
>> to be ashtoningly legit due to vague "for example" wording in the OAuth2
>> spec :-) ).
>>
>>
>> The attack scenario above does not run the redirect scenario in the main
>> browsing context, but in an iframe. Opening an iframe instantiates a new
>> nested browsing context, where unregistered SWs are not available.
>>
>>
>> The service worker, according to
>> https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/fetch_event#description
>>  , just intercepts the authorization code, gets a token, and never sends
>> it back to the main code.
>>
>>
>> This point is not relevant, since your SW is no longer active when the
>> attacker’s authorization code is being returned.
>>
>>
>> But don't trust me on my words: what about demonstrating our claims with
>> actual code, and as such create a shorter, simpler, but more constructive
>> discussion?
>>
>>
>> I don’t understand where this remark comes from. I have already
>> demonstrated back in January that this approach is not effective, and have
>> referred to the recording of this presentation numerous times. I stand by
>> my demonstration of the ineffectiveness of SW and encourage you to test
>> this out yourself. It is as simple as setting up your SW, unregistering it,
>> and running an authorization code flow in an iframe. You’ll see that the
>> request goes through to the AS, and that your SW is no longer able to stop
>> it.
>>
>>
>> The demonstration in its current form would not lead to a successful
>> compromise of a good implementation of access tokens handled by a service
>> worker.
>>
>>
>> Once again, I refer back to the start of my mail: *it is not about
>> protecting existing tokens (scenario 1)**, *it is all about preventing
>> the attacker from running a new flow (Scenario 2).
>>
>>
>> I understand that all of this is quite inconvenient, especially if one is
>> heavily invested in running browser-only OAuth clients. Unfortunately, it
>> is the nature of web-based applications, and doing so requires a complete
>> picture of the security implications. That’s exactly what we are working on
>> with the specification.
>>
>>
>> Kind regards
>>
>> Philippe
>>
>>
>> —
>> *Pragmatic Web Security*
>> *Security for developers*
>> https://pragmaticwebsecurity.com/
>> _______________________________________________
>> OAuth mailing list
>> OAuth@ietf.org
>> https://www.ietf.org/mailman/listinfo/oauth
>>
>>
>
> --
> Yannick Majoros
> Valuya sprl
>
>
>

-- 
Yannick Majoros
Valuya sprl