Closed Bug 1865488 (CVE-2023-6868) Opened 2 years ago Closed 2 years ago

webpush requests do not require vapid key

Categories

(Firefox for Android :: Push, defect)

Firefox 121
Unspecified
Android
defect

Tracking

()

RESOLVED FIXED
122 Branch
Tracking Status
firefox120 --- wontfix
firefox121 --- fixed
firefox122 --- fixed

People

(Reporter: mozbugz, Assigned: teshaq)

Details

(Keywords: reporter-external, sec-moderate, Whiteboard: [reporter-external] [client-bounty-form] [verif?] [adv-main121+])

Attachments

(5 files, 1 obsolete file)

Attached file wpn.tar.gz

In testing webpush to Firefox, I discovered that if you send a push request w/o the request signed w/ the vapid key, even though it was specified in the PushManager.subscribe applicationServerKey.

I have verified that this is required on Chrome. Safari just hangs on the webpush.

Attached is a project that will demonstrate the issue.

extract, run make run to build things, then sudo make run EMAIL=youremail@example.com and go to http://localhost:80/ request notification, and maybe hit reload a time or two.

console output should look similar to:

192.168.0.2 - - [18/Nov/2023 11:03:34] "GET / HTTP/1.1" 200 -                          
'/private/tmp/wpn/templates/../static'
192.168.0.2 - - [18/Nov/2023 11:03:35] "GET /S3_sw.js HTTP/1.1" 200 -                                                                                                          
sub: '{"endpoint": "https://updates.push.services.mozilla.com/wpush/v1/xxx-xxx
-xx-xxx", "expirationTime": null, "keys": {"auth": "xxx", "p256dh": "xxx"}}'  
sending notification...
done                   
Flags: sec-bounty?

I have inadvertently made this public on my mastodon account. I can take it down if you'd like:
https://flyovercountry.social/@encthenet/111432938559622939

Note that the bug can be manually observed after getting the sub by using the included pushnotify.sh script. After getting the sub information above, put the JSON in a file, e.g. sub.info.txt, and then run:

. ./venv/bin/activate
(echo Subject: test title; echo; echo a message) | sh -x pushnotify.sh sub.info.txt

You'll notice it runs the command pywebpush w/ args: -v --data /dev/stdin --info sub.info.txt, but the arguments --claims and --key are not specified.

My test client is Firefox for Android 121.0a1 (Build #2015986545), 6a7bee8e2c+.

Does this happen in a Desktop Firefox as well?

  • If it's just Android then the bug should be moved to the "Push" component of the "Fenix" product
  • If it's both then the bug should be in the "DOM: Notifications" component of the "Core" product
Group: firefox-core-security → core-security
Flags: needinfo?(mozbugz)
Product: Firefox → Core
Product: Core → Firefox
Group: core-security → firefox-core-security

Looking at MDN docs it looks like this is expected: chrome and edge require it, but it's optional in Firefox. Or at least it's optional in PushManager.subscribe(); I don't know if using it is supposed to be optional if you -do- supply it as you said you did.

Kagami: do you know?

Flags: needinfo?(krosylight)

(In reply to Daniel Veditz [:dveditz] from comment #5)

Looking at MDN docs it looks like this is expected: chrome and edge require it, but it's optional in Firefox. Or at least it's optional in PushManager.subscribe();

That sounds wrong, Blink doesn't require it either: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/push_messaging/push_manager.idl;l=14;drc=047c7dc4ee1ce908d7fea38ca063fa2f80f92c77

But per the spec they might reject it during the method call per step 5: If the options argument does not include a non-null value for the applicationServerKey member, and the push service requires one to be given, queue a global task on the networking task source using global to reject promise with a "NotSupportedError" DOMException.

I don't know if using it is supposed to be optional if you -do- supply it as you said you did.

Doesn't seem so per the spec: the push service will reject any push message unless the corresponding private key is used to generate an authentication token.

(In reply to Daniel Veditz [:dveditz] from comment #4)

Does this happen in a Desktop Firefox as well?

  • If it's just Android then the bug should be moved to the "Push" component of the "Fenix" product
  • If it's both then the bug should be in the "DOM: Notifications" component of the "Core" product

Good question, the push service module is different between desktop and android so it's possible that only one side is affected. (I've not confirmed this anywhere though, will do soon)

Flags: needinfo?(krosylight)

I can confirm that for Desktop Firefox, that it requires the vapid key:

ERROR: WebPushException: Push failed: 401 Unauthorized
Response body:{"code":401,"errno":109,"error":"Unauthorized","message":"Missing VAPID public key","more_info":"http://autopush.readthedocs.io/en/latest/http.html#erro
r-codes"}

And that when I provide the key, it works, so it is limited to Fenix.

Group: firefox-core-security → mobile-core-security
Component: Security → Push
Flags: needinfo?(mozbugz)
OS: Unspecified → Android
Product: Firefox → Fenix
Version: unspecified → Firefox 121

Hmm, sounds like the error is coming from our backend server used by the desktop build and not by Fenix (which depends on Google service instead AFAIK).

correct. The above error is showing that desktop push server requires the provided vapid key (and throws an error), unlike mobile firefox.

This command works (cell.sub.txt contains the subscription information for fenix):

pywebpush -v --data /dev/stdin --info cell.sub.txt --key '' --claims ''
[...]
Response:
        code: 200
        body: Empty

<Response [200]>

but shouldn't as it's pushing to Fenix when the public key was requested, but the key and claim are not specified.

This command properly fails as it's pushing to desktop firefox:

pywebpush -v --data /dev/stdin --info desk.firefox.txt --key '' --claims ''
[...]
Response:
        code: 401
        body: {"code":401,"errno":109,"error":"Unauthorized","message":"Missing VAPID public key","more_info":"http://autopush.readthedocs.io/en/latest/http.html#error-codes"}

ERROR: WebPushException: Push failed: 401 Unauthorized
Response body:{"code":401,"errno":109,"error":"Unauthorized","message":"Missing VAPID public key","more_info":"http://autopush.readthedocs.io/en/latest/http.html#error-codes"}
Status: UNCONFIRMED → NEW
Ever confirmed: true
Keywords: sec-moderate

Have Fenix people confirmed this or investigating it? Is there any owner of push component of Fenix?

NI'ing a push backend person in case they have more info.

Flags: needinfo?(jrconlin)

I'm investigating the server, but a few things to note:

The V in VAPID stands for "Voluntary". It's (supposed) to be an optional field provided by the subscription provider to self identify a point of contact if there's an issue. The VAPID Key signature, however, should be used to sign the authorization header.

Where things get a bit odd is that we don't check to see if the aud value matches the originally submitted aud because we don't have to. We should only check that the VAPID key used to sign things is the same. You could use a different value in aud, or since it's all optional, you could leave everything blank and sign an empty JSON array. The bits that matter are the Endpoint URL and the VAPID public key specified as the k component of the Authorization token. We use that key value as a check to make sure that the publisher matches who the App said should be able to send push messages, not the content of the VAPID claims. (This is why if you change your VAPID key, old endpoints no longer work.)

Of course, you can request an endpoint without a VAPID Key signature, and then include a VAPID Authorization header to publish to the Push Endpoint URL, if you wanted. You could also publish to that endpoint without a VAPID header if you wanted.

What you should absolutely not be able to do is request an endpoint with a key signature and then be able to publish a push message to the Push Endpoint URL without a Authorization header or with a Authorization header that uses a different VAPID key. That is checked the same way for both desktop and mobile. (I'm currently verifying that there's not some weird edge case since we recently updated code.)

The server does tell you if a given Endpoint URL has a signature value associated with it.
A registered endpoint that does not have a VAPID key is a /v1/ URL. One that does is a /v2/ URL. (For instance, the Endpoint URL specified in the example in the description was not created with a VAPID key associated with it since no key value was specified either in the Websocket messageType=register command, or the body of the mobile /registration/.../subscription call.)

Flags: needinfo?(jrconlin)

The server code runs all endpoints through the same validation code (desktop or mobile), so I'm not seeing where a signed Endpoint URL would work for desktop but not mobile. Could it be that mobile isn't returning a signed URL?

(I tried using the example code to generate a push notification on Android, but the sample code was not triggering the Notification request. I may try experimenting tomorrow to see if I can replicate what is happening with mobile.)

It looks like firefox android nightly is ignoring the provided VAPID key then. I get a v1 url from Firefox nightly, but a v2 from the desktop..

from firefox for android:

{"endpoint":"https://updates.push.services.mozilla.com/wpush/v1/[...]","expirationTime":null,"keys":{"auth":"[...]","p256dh":"[...]"}}

and from firefox desktop on macosx:

{"endpoint":"https://updates.push.services.mozilla.com/wpush/v2/[...]","expirationTime":null,"keys":{"auth":"[...]","p256dh":"[...]"}}

This is using the exact same JS code for both:

      // (B3) SUBSCRIBE TO PUSH SERVER
      navigator.serviceWorker.ready
      .then(reg => {
      console.log("registered...");
        reg.pushManager.subscribe({
          userVisibleOnly: true,
          applicationServerKey: publicKey
        }).then(
          // (B3-1) OK - TEST PUSH NOTIFICATION
          sub => {
      console.log("pushing...");
            var data = new FormData();
            data.append("sub", JSON.stringify(sub));
            fetch("push", { method:"POST", body:data })
            .then(res => res.text())
            .then(txt => console.log(txt))
            .catch(err => console.error(err));
          },

          // (B3-2) ERROR!
          err => console.error(err)
        );
      });

Thanks! That's very useful.

So, I think I see one potential problem (maybe).

When the client first connects and does a POST to https://push.services.mozilla.com/v1/.../.../registration, the server returns an unsigned v1 endpoint and a channelID

Later, when the client sends a POST to https://push.services.mozilla.com/v1/.../.../registration/.../subscription with the key included in the body JSON, the server returns a .../v2/... endpoint (It also returns a different channelID if one was not specified in the body of the POST)

I'm not sure if Fennec might be returning the first endpoint and channelID, which would cause the bug we're seeing here. To be honest, the server probably should NOT return an endpoint during the first registration event, but I'll need to talk to someone on the client side before the server changes anything.

I believe I found the culprit in https://github.com/mozilla-mobile/firefox-android/blob/b9ed285c624d29cebab5e9da2c3fb9097c2c9e96/fenix/app/src/main/java/org/mozilla/fenix/push/WebPushEngineIntegration.kt#L75-L77 - which is the shim in-between gecko-view and Fenix

That comment is as old as Fenix and I believe things have changed since (i.e. I believe we can and should pass the server key properly now).

Just to be clear, in order to send a notification to a Push user, you still need the endpoint and encryption keys, both of which should not be made public. What this bug does expose is that if someone were to use a non signed endpoint to send a properly encrypted push notification, it would succeed. (Honestly, if your endpoint and encryption keys are compromised, then your VAPID key is probably also compromised.)

Thank You for noting this bug!

Assignee: nobody → teshaq
Status: NEW → ASSIGNED
Attachment #9366919 - Flags: review?(jonalmeida942)
Comment on attachment 9366919 [details] [diff] [review] 0001-Bug-1865488-Adds-server-parameter-to-push-subscripti.patch Review of attachment 9366919 [details] [diff] [review]: ----------------------------------------------------------------- Reviewed this offline with teshaq.
Attachment #9366919 - Flags: review?(jonalmeida942) → review+
Status: ASSIGNED → RESOLVED
Closed: 2 years ago
Flags: qe-verify+
Resolution: --- → FIXED
Target Milestone: --- → 122 Branch
Flags: sec-bounty? → sec-bounty+

Is this something we should uplift to Beta also?

Group: mobile-core-security → core-security-release
Flags: needinfo?(teshaq)

The patch is trivial and low-risk so I'd vote for uplifting it

I don't know the full impact of not having it from a security perspective but it should be safe to uplift (I am happy to be corrected as well by the Android team if this needs to bake more in nightly)

Flags: needinfo?(teshaq)

The patch landed in nightly and beta is affected.
:teshaq, is this bug important enough to require an uplift?

  • If yes, please nominate the patch for beta approval.
  • If no, please set status-firefox121 to wontfix.

For more information, please visit BugBot documentation.

Flags: needinfo?(teshaq)

Comment on attachment 9367331 [details] [review]
[mozilla-mobile/firefox-android] Bug 1865488: Adds server parameter to push subscription (backport #4698) (#4736)

Beta/Release Uplift Approval Request

  • User impact if declined: Push subscription endpoint wouldn't be signed using the VAPID key, and thus can be triggered by arbitrary senders if they acquire the endpoint and the encryption key.
  • Is this code covered by automated tests?: Unknown
  • Has the fix been verified in Nightly?: Yes
  • Needs manual test from QE?: No
  • If yes, steps to reproduce:
  • List of other uplifts needed: None
  • Risk to taking this patch: Low
  • Why is the change risky/not risky? (and alternatives if risky): It's a trivial (and small) patch that passes the application server key provided by consumers of web push to the autopush server.
  • String changes made/needed:
  • Is Android affected?: Yes
Flags: needinfo?(teshaq)
Attachment #9367331 - Flags: approval-mozilla-beta?
Comment on attachment 9367331 [details] [review] [mozilla-mobile/firefox-android] Bug 1865488: Adds server parameter to push subscription (backport #4698) (#4736) Approved for 121.0b9.
Attachment #9367331 - Flags: approval-mozilla-beta? → approval-mozilla-beta+

Hello,
Is there any QA manual verification needed for this issue?
Thank you!

Flags: needinfo?(jonalmeida942)

Hi Mira, I don't think so. Thanks for checking!

Flags: needinfo?(jonalmeida942)
Flags: qe-verify+
Whiteboard: [reporter-external] [client-bounty-form] [verif?] → [reporter-external] [client-bounty-form] [verif?] [adv-main121+]
Attached file advisory.txt (obsolete) —

If you want to add a bit more color to the text of the advisory, obviously should be reviewed by someone like JR Conlin [:jrconlin,:jconlin] who knows WebPush better (I only started learning/using it a month or so ago), but:

The confidentiality of push notifications are not impacted by this vulnerability.

The VAPID key is used by the Web Push service to restrict the server that is allowed to push notifications to the client, and allow the Web Push service to contact the server in case there are issues with push notifications (e.g. too many, bad requests).  If the Push URL is leaked, (e.g. database compromise), it would allow third parties to send PUSH notifications (which the web service worked may not handle correctly).

This SO seems to provide more information.
https://stackoverflow.com/questions/40392257/what-is-vapid-and-why-is-it-useful

The RFCs are also useful: https://www.rfc-editor.org/rfc/rfc8291
and:
https://www.rfc-editor.org/rfc/rfc8292

(which the web service worked may not handle correctly)

Might need to fix that up a bit, but yeah.

Honestly, the one possible exploit that could be done would be:

  1. Create a service worker that takes a Data Free push message as it's action, store the unsigned /v1/ URL.
  2. Leak that specific /v1/ Push URL.
  3. Evil Party uses that /v1/ URL to trigger your webapp action.

Data free Push messages do not require encryption and thus could be open for anyone to use.

(heh, honestly, when I wrote that article not a lot of folk really knew what made VAPID useful. We've learned a lot since then.)

Attached file advisory.txt
Attachment #9367954 - Attachment is obsolete: true
Alias: CVE-2023-6868

Just a FYI, I have verified that Fenix nightly (123.0a1) gives a v2 url, and that if the vapid key isn't used, the server returns a 401 vapid key required, and that when a vapid key is provided, that notifications work.

Bulk-unhiding security bugs fixed in Firefox 119-121 (Fall 2023). Use "moo-doctrine-subsidy" to filter

Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: