Maintainer can leak masked webhook secrets by manipulating URL masking

Please read the process on how to fix security issues before starting to work on the issue. Vulnerabilities must be fixed in a security mirror.

HackerOne report #1976206 by theluci on 2023-05-07, assigned to @nmalcolm:

Report | Attachments | How To Reproduce

Report

Hello,
I recently submitted a report #1915507 which was closed as duplicate of #391685 (closed).
The fix to #391685 (closed) can be bypassed.

Summary

There is an option to mask parts of a webhook URL to treat it as a secret value.
https://docs.gitlab.com/ee/user/project/integrations/webhooks.html#mask-sensitive-portions-of-webhook-urls

When this feature is used any secret string in the configured URL will be masked in the UI and in any logs in the UI. The values work the same as other tokens in that they are not even accessible by the user configuring it after it is first configured. It should not be possible for the initial user or any other users to retrieve these values.

The docs states this about the secret

Sensitive portions do not get logged and are encrypted at rest in the database.

However, there is a way to leak masked webhook secrets by bypassing the validation logic. (See Vulnerability)

Vulnerability

The fix changed validation logic as following,

  def reset_url_variables  
    interpolated_url_was = interpolated_url(decrypt_url_was, url_variables_were)

    return if url_variables_were.empty? || interpolated_url_was == interpolated_url

    self.url_variables = {} if url_changed? && url_variables_were.to_a.intersection(url_variables.to_a).any?  
  end  

To bypass the validation logic

  1. attacker mask the victim-url such as https://victim-url?token={TOKEN} becomes {masked-url}?token={TOKEN} and clicks save.

This will not reset url_variables as url_changed? returns false.

  1. attacker now changes the url such as {masked-url}?token={TOKEN} becomes https://attacker-url?token={TOKEN}
  2. attacker masks the attacker-url with the same mask {masked-url}. So, that url finally becomes {masked-url}?token={TOKEN} and clicks save.

This will not reset url_variables as interpolated_url_was == interpolated_url condition is satisfied.

  1. attacker can now click test to receive masked webhook secrets.

Steps to reproduce

victim is the owner of a project victim-project
attacker is a maintainer in victim-project

As victim,

  1. victim goes to his victim-project webhook settings, https://gitlab.com/<victim-group>/<victim-project>/-/hooks
  2. victim configures a webhook with a secret token and mask the secret token. For example, Put the URL like this https://example.com?token=secret-token
  3. Click to add a mask, add the value secret-token with the replacement TOKEN. Click save for the webhook

W1.png

As attacker

  1. attacker goes to victim-project webhook settings, https://gitlab.com/<victim-group>/<victim-project>/-/hooks
  2. Scroll to the bottom of the page to the list of configured hooks and edit the webhook.
  3. attacker mask the victim-url as {masked-url}. Keep the token={TOKEN} part. Like this, {masked-url}?token={TOKEN} and clicks save

W2.png

  1. attacker add the attacker-url (i used https://webhook.site to catch the request). Keep the token={TOKEN} part. Like this, https://attacker-url?token={TOKEN} and mask the attacker-url using the same mask, {masked-url} as shown below

W3.png

  1. attacker click save
  2. attacker click test
  3. Request is sent to the attacker-url and containing the secret token.

W4.png

POC

poc-webhook-2.mp4

Impact

Maintainers can leak secret masked values that should not be accessible after configuration.

Output of checks

This bug happens on GitLab.com (Probably on instance too)

Impact

Maintainers can leak secret masked values that should not be accessible after configuration.

Attachments

Warning: Attachments received through HackerOne, please exercise caution!

How To Reproduce

Please add reproducibility information to this section: