Hi, this bug is one of my older reports - a bug in a Google acquisition called AppSheet. AppSheet is a no-code app generator. With this service, you can create applications from your browser without having deep knowledge of App development.
SSRF in webhook
I looked into the AppSheet features and found a section called Workflows (today replaced by "AppSheet Bots"). AppSheet Workflow made it possible to automate app behaviour by defining specific rules. For example - send an email notification when a user creates a new row in a table. There is a variety of options for defining these rules. One of those options is to call a webhook on the rule trigger. That sounded promising, so I looked into this feature :).
Workflow rule settings |
I created a new workflow with the rule: call a webhook when data in a table are changed. One of the first things I tried was to call a metadata API. Well, I bumped into a first problem. There wasn't an option to create a GET request, which is needed to obtain metadata (or at least I thought so). I could make only requests with POST, DELETE and PATCH. After thinking and googling for a while, I got an idea to use redirection to create a GET request from POST.
In the HTTP redirect documentation, I found that some redirect types "may or may not" change the HTTP method to GET. I tested it with a few HTTP clients and libraries, and actually, all of them changed the method to GET. Exactly what I needed.
So on a domain owned by me, I created a 301 redirect (using the Location header):
http://<MY_DOMAIN>/<ROUTE> to http://169.254.169.254/<ROUTE>
The expected behaviour was that the AppSheet HTTP client would change the POST method to GET:
POST http://<MY_DOMAIN>/<ROUTE> to GET http://169.254.169.254/<ROUTE>
I used this redirect in a webhook with the following setup (request is pointing to legacy metadata API):
POST http://<MY_DOMAIN>/computeMetadata/v1beta1/instance/service-accounts/default/token
Webhook setup |
I saved the workflow and triggered the action by adding a new row into a table in an AppSheet app.
After a few minutes, I opened the application logs and found the record for the webhook call. The logs showed a response body from the metadata server. In the response was the access token for the AppSheet application.
Webhook logs |
I reported the bug to Google, and it got accepted, rewarded and marked as fixed.
Bypassing the fix
I decided to retest the fix. This time I created the same webhook and found out that the legacy API I used was disabled (/v1beta1/). So naturally, I tried sending the request to the current metadata API, which requires added security header (there was an option to append headers in webhook form, so it didn't look like an issue).
I created a webhook:
POST http://<MY_DOMAIN>/computeMetadata/v1/instance/service-accounts/default/token
Header: Metadata-Flavor: Google
I saved it, triggered the action and looked into the logs. This time I saw in a log that the server returned a Forbidden error.
Anyway, I played around with the webhook. I assumed that the request reached a metadata server because there was a successful response when calling http://169.254.169.254/computeMetadata/ (this endpoint does not require the security header - there is no valuable or sensitive information). I thought that the error response was returned because the Metadata-Flavor header got filtered from the request. But, there are two types of security headers that can be added. The first one is the mentioned Metadata-Flavor, and the other option is header
X-Google-Metadata-Request: True
Also, another thing I found during testing is that the metadata endpoint for obtaining an access token supports both GET and POST methods; this information is not in the documentation (so my research on redirects was not necessary, but since it might be helpful for some of you, I left it here :).
The final webhook I created was:
POST http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token
Header: X-Google-Metadata-Request: True
I looked into the logs and found there again the access token, so only one of the security headers was filtered.
I reported the bypass; it got accepted and fixed. The fix now is (probably) that the security header I used is removed from the request as well.
Reported: October 2020
Reward: $3133,7 + $3133,7 (for initial bug and the bypass).