如何在浏览器中存储会话令牌

  • A+
所属分类:安全文章

如何在浏览器中存储会话令牌

最近有一条关于OWASP ASVS的提议变更的推文引发了一场激烈的争论,并且挑战了我在构建和设计单个页面应用程序时对存储会话令牌的不同策略的理解。虽然以前有很多关于这方面的文章,但我也有了自己的研究。

为此,我决定制作一系列的“概念验证”单页应用程序来说明每一个不同的策略。我还希望这些完全是准PoC,实际上,大多数SPA可能会利用现有的框架和库,但我发现这些抽象使抓取概念变得困难,因此我没有使用任何JS框架,只是使用简单,易于遵循的原始JavaScript。

为了帮助说明关于ASVS的争论,我还在每个页面中添加了一个简单的“XSS”,并将展示攻击者控制的JS在这些页面中能够产生的影响。

我的“app”由两个API组成:

/api/login:这会生成并返回一个cookie或JSON数据的“会话令牌”,没有用户名/密码。

/api/echo:这个端点简单地用它收到的任何令牌进行响应,如果没有发送令牌,则表示“未授权”,假设这只是一个经过身份验证的端点。

因此,打开一个新的标签页到https://tokenstorage.ropnop.dev并继续操作!

如何在浏览器中存储会话令牌
Cookies

PoC页面:https://tokenstorage.ropnop.dev/cookie.html

源代码:https://github.com/ropnop/tokenstorage/blob/master/cookie.html

这是一种“经典”方法,早于单页面应用程序。在这个场景中,在POST 'ing到/api/login之后,端点用Set-Cookie标头中生成的会话令牌进行响应。你可以通过点击“Login and get a new token”来验证这一点,并在Developer Tools中查看响应:

如何在浏览器中存储会话令牌

在本例中,我们将会话令牌的“所有权”委托给浏览器。浏览器识别设置的值,并将其保存在“cookie jar”中,这也可以在开发人员工具中查看:

如何在浏览器中存储会话令牌

我们的客户端代码不需要对/ api / login中的响应主体进行任何操作,由于浏览器已为我们保存了cookie,因此无需捕获或记住任何内容。只要该cookie值存在并且没有过期,浏览器就会自动将该值发送到与域和路径集匹配的任何端点。从开发的角度来看,这是很棒的,我们甚至不需要考虑自己处理身份验证,浏览器将仅发送任何匹配的cookie,并且可以检查服务器端,但这也会带来一些不良的意想不到的后果(例如CSRF)。

在我们的客户端代码中,我们根本不需要做任何特殊的事情来发送经过身份验证的请求,浏览器将为我们发送cookie(如果存在的话)。所以我的原始JS API调用看起来像这样:

如何在浏览器中存储会话令牌

我只需要在资源上使用fetch,它就能工作。你可以通过在页面中点击 ‘Make an “authenticated” request’ 来验证这一点,并看到服务器响应发送的cookie值。你可以通过查看开发人员工具中的请求标头来验证Cookie是“自动”发送的:

如何在浏览器中存储会话令牌

如何在浏览器中存储会话令牌
XSS的影响

因为这个cookie是用HttpOnly设置的,所以任何客户端JavaScript代码都无法访问它。如果攻击者在我们的SPA上获得了XSS,那么攻击者就无法读取cookie值。你可以通过在XSS框中输入alert(document.cookie)来进行尝试,以确保无法读取或显示任何值:

如何在浏览器中存储会话令牌

然而,重要的是要记住,即使攻击者不能读取cookie值,他或她仍然可以使用cookie值。因为浏览器将cookie与每个请求一起发送到匹配的域,如果我们的XSS有效载荷是fetch("/api/echo"),那么cookie将被自动发送,攻击者可以读取响应。

如何在浏览器中存储会话令牌
Cookies的特点

优点

容易实现,无需自定义客户端(浏览器会为我们处理身份验证);

如果设置HttpOnly,则无法从JS访问;

如何在浏览器中存储会话令牌
缺点

自动发送,可能会导致意外后果(CSRF);

不能跨域发送;

如何在浏览器中存储会话令牌
持久性

在新的页面、标签、刷新等情况下仍然存在;

持续直到故意删除或过期,通过Cookie MaxAge或Expires属性从服务器控制;

如何在浏览器中存储会话令牌
本地存储

PoC页面:https://tokenstorage.ropnop.dev/localStorage.html;

源代码:https://github.com/ropnop/tokenstorage/blob/master/localStorage.html;

在这个示例和下面的示例中,服务器以JSON主体的形式响应会话令牌,这意味着由我们(客户机)来管理它。一种方法是使用浏览器的LocalStorage API,这是范围限定于源的持久性存储。

在客户端代码中,令牌响应在点击“Login”按钮(下面第11行)后保存到localStorage:

如何在浏览器中存储会话令牌

现在已在localStorage中设置了令牌,我们可以在开发人员工具中进行验证:

如何在浏览器中存储会话令牌

将令牌存储在本地存储中后,令牌将再次由客户端发送(通常在“授权”标头中)。如果存在,我们必须从localstorage中获取它(第3-4行),然后使用我们的请求发送自定义标头(第6行):

如何在浏览器中存储会话令牌

与cookie方法相反,我们必须明确地在客户端代码中发送自定义标头。

如何在浏览器中存储会话令牌
XSS的影响

不幸的是,LocalStorage不提供XSS保护,由于该值需要由JavaScript读取,因此任何在同一来源(即XSS)执行的“恶意” JavaScript都将对本地存储中的所有内容进行完全读取/写入。通过运行alert(window.localStorage.token)自己进行验证:

如何在浏览器中存储会话令牌

如何在浏览器中存储会话令牌
本地存储的特点

优点

持续刷新/关闭页面;

作用域为原点,而不是域;

从不自动发送到任何地方(CSRF不可能);

如何在浏览器中存储会话令牌
缺点

通过恶意JS (XSS)轻松窃取

持久性

在新的页面、标签、刷新等情况下仍然存在;

持续到故意删除为止;

如何在浏览器中存储会话令牌
会话存储

PoC页面:https://tokenstorage.ropnop.dev/sessionStorage.html;

源代码:https://github.com/ropnop/tokenstorage/blob/master/sessionStorage.html;

这几乎与LocalStorage相同,但有一个重要的区别:SessionStorage不能跨浏览上下文持久存储,这意味着如果关闭页面,它将被删除。

设置令牌几乎与本地存储相同,我们只是在检索令牌后使用其他API保存令牌:

如何在浏览器中存储会话令牌

设置完成后,我们可以再次在开发人员工具中对其进行验证:

如何在浏览器中存储会话令牌

要发送经过身份验证的请求,我们再次必须从SessionStorage检索它并将其作为自定义标头发送:

如何在浏览器中存储会话令牌

同样,LocalStorage和SessionStorage之间的唯一区别是持久性。如果关闭窗口或打开一个新窗口,则令牌值将从SessionStorage中消失,而与LocalStorage相反。

如何在浏览器中存储会话令牌
XSS影响

就像LocalStorage一样,SessionStorage提供针对XSS的零保护,因为它旨在通过JavaScript进行读写。你可以通过在PoC页面中运行alert(window.sessionStorage.token)进行验证:

如何在浏览器中存储会话令牌

如何在浏览器中存储会话令牌
会话存储特点

优点

作用域为原点,而不是域;

从未自动发送到任何地方(CSRF impossible);

如何在浏览器中存储会话令牌
缺点

通过恶意JS (XSS)轻松窃取;

如何在浏览器中存储会话令牌
持久性

仅适用于当前浏览上下文

是否在刷新过程中持续存在

页面关闭或在新页面/标签上丢失

如何在浏览器中存储会话令牌
全局变量

PoC页面:https://tokenstorage.ropnop.dev/globalVar.html;

源代码:https://github.com/ropnop/tokenstorage/blob/master/globalVar.html;

在接下来的几个示例中,我们将完全不使用任何浏览器存储,而仅依靠运行中的客户端JavaScript来存储、设置和检索会话令牌。

实现此目的的最简单方法是仅使用顶级全局变量,我们需要的任何函数中的任何正在运行的客户端代码都可以使用此变量。

为了演示,我在窗口上设置了一个顶级变量token,并在从登录API中检索到令牌时进行了设置:

如何在浏览器中存储会话令牌

要将其设置为Authorization标头,我只需要引用它的变量名:

如何在浏览器中存储会话令牌

这很容易实现,并且变量在需要时可用。这将无法在页面刷新或新的浏览上下文中保留,当JS重新加载时,这个变量就会被删除。

如何在浏览器中存储会话令牌
XSS的影响

由于令牌只是一个JavaScript变量,所以如果攻击者获得了恶意的JS执行,那么读取该值是很容易的。他或她唯一需要做的就是通过查看客户端代码找出变量的名称,为了验证你自己,试着在XSS框中输入alert(window.token)或alert(token):

如何在浏览器中存储会话令牌

如何在浏览器中存储会话令牌
全局变量特点

优点

容易实现;

令牌值只存在于“内存中”;

可以跨域发送;

如何在浏览器中存储会话令牌
缺点

容易被恶意JS (XSS)窃取;

如何在浏览器中存储会话令牌
持久性

在页面刷新或新建页面时丢失

如何在浏览器中存储会话令牌
闭包变量

PoC页面:https://tokenstorage.ropnop.dev/privateVar.html;

源代码:https://github.com/ropnop/tokenstorage/blob/master/privateVar.html;

现在,我们终于可以找到我认为是在浏览器中存储会话令牌的最安全方法。在此示例中,我们再次将会话令牌值仅保留在“内存中”,但对其进行保护,以便可以使用其值,但实际上从未从任何其他JavaScript读取过,可以通过将其存储在闭包内部来实现此目的(如果有帮助,可以将其视为类似于类内的私有变量)。

从架构上讲,我们唯一需要会话令牌的是发送HTTP请求,因此我们可以设计闭包来公开一个fetch函数,该函数会自动附加令牌值。我们需要公开的另一件事是设置令牌的方法。

如何在浏览器中存储会话令牌

这个名为authModule的闭包只公开了两个函数:setToken和fetch。在设置标记值之后,就不可能再次读取它了。这是关闭。fetch函数模仿真实的fetch函数,但是如果目标源与白名单匹配,它将附加授权头。这一点非常重要,因为如果你没有进行此检查,此模块将向任何域发送授权头,而攻击者可能通过XSS将敏感令牌发送给自己来滥用此消息。

在登录之前,我们现在实例化一个新的authModule(1),并在检索令牌时将其设置为(13):

如何在浏览器中存储会话令牌

现在,每当我们希望发送经过身份验证的请求时,我们都可以使用auth.fetch而不是普通提取(我添加了一个自定义标头,只是为了演示它们也被发送了):

如何在浏览器中存储会话令牌

如何在浏览器中存储会话令牌
XSS的影响

在这种情况下,XSS的影响非常小,就像HttpOnly cookie一样,令牌本身的值不可能使用JavaScript提取。攻击者可以使用身份验证。获取其XSS有效载荷中的内容,但它们只能将授权标头发送到白名单源。随时尝试通过XSS检索令牌值,如果有人可以找到一种访问令牌的方法。

如何在浏览器中存储会话令牌

@coffeetocode演示了一个出色的XSS有效载荷,它覆盖了正常的获取操作以窃取授权cookie。我已经更新了代码,以在闭包中包含受保护的fetch副本:

如何在浏览器中存储会话令牌

如何在浏览器中存储会话令牌
闭包特点

优点

令牌值只存在于“内存中”;

可以跨域发送;

令牌值不受其他JS代码的影响;

对令牌在何处/何时被发送的准确控制;

如何在浏览器中存储会话令牌
缺点

实现稍微复杂一些

如何在浏览器中存储会话令牌
持久性

在页面刷新或新建页面时丢失

如何在浏览器中存储会话令牌
Service Worker

PoC页面: https://tokenstorage.ropnop.dev/serviceWorker.html;

源代码:

https://github.com/ropnop/tokenstorage/blob/master/serviceWorker.html;

https://github.com/ropnop/tokenstorage/blob/master/js/serviceWorker.js;

我要讨论的最后一种方法可能是最复杂的,但在我看来是在浏览器中处理会话令牌最酷的方法,那就是通过使用Service Worker。在此之前,我没有太多使用Service Worker的经验,但它们是标准Web API的强大补充。Service Worker本质上是在自己的上下文中执行的浏览器代理服务器中,并且在刷新和新页面加载之间持久存在。我们可以使用一个Service Worker来为我们记住会话令牌,然后为任何需要它的网络资源发送会话令牌。

首先,我们需要在希望Service Worker监视的页面上注册Service Worker。在我们的SPA中,如下所示:

如何在浏览器中存储会话令牌

在第4行中,我将范围限制为仅此特定页面,但在实践中,我们可以使用/代替在每个页面上运行此操作。

实际的ServiceWorker代码有点复杂,但是核心函数应该类似于上面的闭包逻辑。有一组列入目的地清单的白名单,并且我还为路径实现了正则表达式:

如何在浏览器中存储会话令牌

我们需要使用postMessage将令牌值发送给Service Worker,而不是像在闭包示例中那样创建公开的函数来设置令牌值。由于Service Worker在其自身的上下文和来源中运行,因此它充当了一种“ RPC”的角色。因此,我们设置了一个侦听器来接收令牌值并进行设置:

如何在浏览器中存储会话令牌

现在,回到我们的SPA,我们将登录后收到的token值发送给服务人员:

如何在浏览器中存储会话令牌

现在Service Worker拥有了标记值并将记住它。

为了使用令牌,我们可以使用Service Worker的魔力来拦截每个fetch调用,并确定是否需要添加令牌值。我们通过设置一个事件监听器来获取Service Worker:

如何在浏览器中存储会话令牌

运行此事件侦听器后,如果将目的地列入白名单,我们在SPA中进行的所有正常提取操作都会自动发送秘密令牌。因此,我们可以使用正常的提取操作,而不必担心身份验证(有点像可以使用cookie):

如何在浏览器中存储会话令牌

继续并在PoC页面上尝试一下。关闭页面并刷新,然后尝试发送经过身份验证的请求而不请求新令牌,服务人员会记住先前的令牌!

如何在浏览器中存储会话令牌
XSS的影响

与闭包类似,这里不存在XSS影响。据我所知,SPA中的XSS无法访问或修改Service Worker内部的值。攻击者唯一能做的就是通过postMessage设置一个假的令牌值,或者使用fetch从受害者的浏览器发送一个经过身份验证的请求。

如何在浏览器中存储会话令牌

如何在浏览器中存储会话令牌
Service Worker的特点

优点

Token值只存在于service worker中;

可以跨域发送;

令牌值不受其他JS代码的影响;

准确控制令牌在何处/何时被发送;

令牌会自动添加,没有特殊的客户端代码可发送已验证的请求

如何在浏览器中存储会话令牌
缺点

实现起来要复杂得多

如何在浏览器中存储会话令牌
持久性

持续刷新和加载新页面;

持续到Service Worker取消注册/更新为止;

如何在浏览器中存储会话令牌
总结

我希望这有助于说明在浏览器中存储和发送秘密令牌的不同方法背后的概念,本文的目标是通过尽可能简单的PoC演示每个选项的示例,然后评估每个选项的XSS影响和持久性。

参考及来源:https://blog.ropnop.com/storing-tokens-in-browser/

如何在浏览器中存储会话令牌

如何在浏览器中存储会话令牌

本文始发于微信公众号(嘶吼专业版):如何在浏览器中存储会话令牌

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: