Stealing OAuth tokens of Microsoft accounts via open redirect in Harvest App
eval.blog
eval.blog
The truth here is that we were never able to fully reproduce the issue from the beginning, but struggled to close it because of the fear of missing something. Shortly after when we got back to the reporter for the last time, saying that we'll find a resolution, is when we were convinced that we were not able to reproduce it. Around that time we received a similar OAuth-related report. Unfortunately, this led to an internal mix-up, making us believe that we had addressed and communicated the resolution.
Because of the way I have notifications set up, I missed the follow-ups, and the issue stayed in Triage state indefinitely without receiving updates. This is by no means an excuse about the lack of updates, about which I'm deeply sorry. I've been a bug bounty hunter for many years and understand how frustrating it is to wait for updates from companies.
Finally, I'd like to reassure y'all that the security of our customers is of the utmost importance to us, and everything we say in our security page is true.
By the time the report was originally sent the feature was just released, and while we never deployed a code change to directly address it, it wouldn't be the first time that we receive something that I believe it was genuinely a security issue and stopped being reproducible due to an seemingly unrelated change around the same time.
So for three years you believed there was something, yet you didn't invest sufficient resources to reproduce and/or understand the issue, while at the same time, all these three years security was of utmost importance?
It's still unclear what prevented the follow up communications from making its way to you.
The app state constantly gets out of sync with server state (some changes on the server only show up after a force reload, some changes on the client just revert after pressing save)
And the time tracking UX is so annoying (buttons that are only visible if you scroll down, start/stop/restart/delete buttons are constantly at different locations, depending on the state of the item).
The old app was not pretty, but it worked without issues.
And I would doubt that those are my "personal" UX issues.
Apparently Harvest is it's own company, not owned or operated by MS.
Also, it is still unclear how you wanna continue with the report since it is no longer reproducible. I would have discussed it further on Hackerone but apparently I have been ghosted again after the apologize message.
I started getting spammed as a "user of Harvest" which prompted me to suspect that they were selling their customer lists. They took this claim extremely seriously, connecting me with company heads immediately to issue stern denials and execute a prompt investigation. That was great.
What I think it came down to, though, was also engineering. I figured out a rather easy way to reliably infer active customers, which also, "went on the backlog" and remains unfixed months later. And it's a fix that appears to be super trivial.
They also only offer MFA if you're signing in with Google [2]. But the app itself is DAMN good at what it does.
1: https://www.getharvest.com/about/meet-the-team
2: https://support.getharvest.com/hc/en-us/articles/36005266713...
These are in fact harvest's tokens, which only erroneously exposed access to their app, because of an injection vuln in their code, and would be exactly as compromised behind any other IdP.
If you want to add any editorializing around mitigation, linking to the OAuth RFC[0] that dictates a MUST for binding the users auth state with the request to prevent such attacks would be instructive to readers.
[0] https://datatracker.ietf.org/doc/html/rfc6749#section-10.12
Updated to "Stealing OAuth tokens of connected Microsoft accounts via open redirect in Harvest App"
(Submitted title was "Microsoft Account's OAuth tokens leaking via open redirect in Harvest")
The authorization server MUST require public clients and SHOULD require
confidential clients to register their redirection URIs. If a redirection
URI is provided in the request, the authorization server MUST validate it
against the registered value.
So how is this possible, when presumably the Harvest app did not register the malicious redirect_uri?Does the Microsoft OAuth server ignore URL parameters within a redirect_uri when comparing with registered redirect URIs for the OAuth client?
From the POC authorization URL, the redirect_uri parameter and value are:
redirect_uri=https%3A%2F%2Foutlook-integration.harvestapp.com%2Fauth%2Foutlook-calendar%2Fcallback?state=%7b%22return_to%22:%22/time%22%2c%22subdomain%22:%22example.com/%22%7d
So if Harvest registered the redirect_uri as: https%3A%2F%2Foutlook-integration.harvestapp.com%2Fauth%2Foutlook-calendar%2Fcallback
then why does any extra URL parameters added to that value get accepted by the Microsoft OAuth server before authorization, when they clearly do not match the registered one?edit: I tried authorizing using another OAuth server provider, with a changed redirect_uri by appending URL parameters to the encoded value, and the OAuth server (I believe, quite rightly) rejected the authorization request.
Go the some harvest authorize url,
That redirects to the Microsoft authorize url with redirect_uri=registered_uri and state=some_encoded_final_uri,
user enters credentials,
redirect to a registered uri
read state parameter and redirect to uri encoded in state.
This exploit still redirect to an authorized uri, but that endpoint then reads the the state parameter and happily forwards the response/token.
3 mistakes in this, abusing state, not encypting and validing state if you are going to abuse it. Enabling implicit grant(even if they needed it, should have made a second registration with limited uses).
For example, if you're making a shopping website and a user asks to put something in their basket and you send them to log in, you'd want to return them to the item they were about to buy, not dump them back at the homepage.
What's the proper way of doing this, without "abusing state" ?
Ideally you would 'consume' the token before redirecting, and not send it to the second redirecting url.
If for some reason you really do need to transmit this data in-band (ultra rare use case) you should at least be using something like HMAC to verify that all carriers have transported the data unmodified. It is your responsibility to ensure the integrity of the data end-to-end.
But some providers allow query parameters. For Microsoft, it was possible in 2020 when I reported the vulnerability. In 2022, they restricted query parameter support to only applications that is built for Work and School accounts and in August 2022, they added a section for this in the documentation.
See: - Commit: https://github.com/MicrosoftDocs/azure-docs/commit/c249a0548... - Current Documentation: https://learn.microsoft.com/en-us/azure/active-directory/dev...
FYI, they are omitting it in the upcoming OAuth 2.1 spec: https://www.ietf.org/archive/id/draft-ietf-oauth-v2-1-09.htm...
From my very limited OAuth knowledge isn't this how it works:
1. The Harvest application asks Microsoft to verify a user. 2. The user is verified by Microsoft. 3. If the user verification is successful Microsoft redirects back to the callback URL, passing back the access token inside the body of the response message.
In this case hasn't the writer of the blog just created a hand-crafted URL so that the return is back to example.com rather than the actual return URL?
This issue seems to be that there was a secondary redirect in the body of one of the requests (I believe the token response), that could be forged to loosely match a trusted domain but with an attacker’s domain present, eg “//attacker.com/trusted.com/“.
> The authorization server SHOULD require the client to provide the complete redirection URI (the client MAY use the "state" request parameter to achieve per-request customization). If requiring the registration of the complete redirection URI is not possible, the authorization server SHOULD require the registration of the URI scheme, authority, and path (allowing the client to dynamically vary only the query component of the redirection URI when requesting authorization).
> The authorization server MAY allow the client to register multiple redirection endpoints.
https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2....
Either the redirect URL is statically configured, or it's accepted as a query param to the auth request, and subject to a strict whitelist. It's not a secret from the user, but even for a SPA it is usually transient so you don't have the user sitting at some ugly URL with "?code=abc123...". Typically you would use the state query param to retain any context needed to redirect the user to their desired destination, but that would be after the redirect endpoint uses the passed code to fetch the token and store it somewhere locally. In this case apparently the redirect endpoint allowed redirecting to entirely different applications by simply forwarding on the sensitive query params, but did not validate that those destinations were on any whitelist.
That url allows you to link someone to a login.microsoftonline.com link, have a login prompt show up that says "login to harvestapp", and then have the attacker be able to gain permissions related to your real harvestapp account.
Normally, this would not be possible. The attacker with example.com could register a new app that does redirect to example.com, but that would not give them an access token with permissions related to harvestapp, so it would not be useful.
The oauth app, on microsoft's end, has a whitelist of valid redirects, so an attempt to do something like "login.microsoftonline.com/authorize?client_id=$harvestAppID&redirect_uri=attacker.com" will error out on microsoft's side, since that is not a valid redirect uri to receive an access token.
The attack is only possible because there's a valid "outlook-integration.harvestapp.com" URL, which receives the access token, but then also redirects to the attacker's site and gives them the access token too.
The property `subdomain` was used to redirect the browser to a subdomain of harvestapp.com, passing the `#id-token`. The problem came from the fact that the value of `subdomain` was injected directly to:
https://${subdomain}.harvestapp.com/...#id-token=...
By setting the `subdomain` in JSON payload to `attacker-controlled.com/` (note the trailing slash), the URL become:
https://attacker-controlled.com/.harvestapp.com/...#id-token=..
..thus redirects the browser to another domain, leaking the token.1. Make sure the redirect url is a valid harvestapp.com (more checks on state)
2. Encrypt the state since the start of the request, so then they can double check the state hasn't been forged by decrypt and compare
Is there any option beside those?
* the additional redirect using the JSON object in state * the `subdomain` not being properly verified * the implicit grant being supported
Which allowed an attacker to get an access token for a user's Microsoft account.
From my reading, this seems to be entirely an issue due to an improper implementation on Harvest's side, nothing to do with Microsoft's implementation of OAuth. Am I correct?
I assume that for several years though, that was exactly what Microsoft thought too.
It seems pretty clear to me from reading the blog post that the issue was what I outlined (sorry for the lack of list formatting, I always forget I need an extra line after each bullet point).
However that additional leeway should be afforded by the researcher and/or their lawyers / representatives. It's something a company might ask for in good faith in response to a larger than usual issue.
In the vast majority of cases, companies deny requests for public disclosure. A researcher that discloses regardless of permission violates their agreement with hackerone and the company and exposes themselves to legal liability. In this case it seems the company agreed to public disclosure, which IMO should be applauded, even if their response was very slow.
I've personally had several four figure bugs unremediated for >1year, but I never thought it was hackerone's fault.
A key part of responsible disclosure is the disclosure part.
Often researchers would disclose unpatched issues to put weight on companies, even large companies, to actually patch issues.
One of the side-effects of programs like Hackerone is that actually doing your own responsible disclosure is now frowned upon (often to the point of legal problems).
But part of the social contract of absorbing coordinated disclosure should be an expectation that hackerone allows disclosing even unfixed issues.
Hackerone should not be "beholden" to companies. They make the rules. They could allow disclosure of issues if they wanted to make that a condition of the platform.
It's companies sitting on vulnerabilities that birthed the concept of "responsible disclosure" in the first place. If H1 etc are allowing it then there needs to be renaisance of the practice outside the platforms.
Feel free to post all research results to f-d in full. This is a reasonable and responsible way to notify companies about vulnerabilities.
As for the violation of agreement with hackerone, I have read the policy many times before publishing the article and even asked Hackerone about this. The vulnerability is already fixed and I haven't heard from Harvest since April 2022 so there's no point asking them as it would seem like a threat rather than an actual disclosure. An excerpt from the agreement:
> Last resort: If 180 days have elapsed with the Security Team being unable or unwilling to provide a vulnerability disclosure timeline, the contents of the Report may be publicly disclosed by the Finder. We believe transparency is in the public's best interest in these extreme cases.
It seems very reasonable to me that if the decision to leave HackerOne is prompted by conflict over responsible disclosure, then it is appropriate for HackerOne to disclose that fact. Including disclosing the bugs that the company was unwilling to responsibly disclose.
This puts HackerOne in the position of actually representing the interests of the hackers. And makes participating in HackerOne to be more than a meaningless publicity gesture for the companies.
HackerOne didn’t care. No matter how many times we pointed out the person was violating their own rules, they claimed they couldn’t do anything.
It felt like a company that had been built up to steady state operations, then stripped down to a bare minimum operating crew where questions were answered by powerless support people.
This was a while ago. Maybe things have changed, but that was my impression at the time.
Although there are probably thousands of similar bad implementations out there that are connected to Microsoft via oauth.
I have no idea if Microsoft would react to such a report, and what's the correct channel to submit it. But bug reports or abuse reports they usually take seriously.
[1] https://arstechnica.com/security/2023/10/okta-says-hackers-b...
[2] https://en.wikipedia.org/wiki/Okta,_Inc.#Security_incidents
- Generate an encrypted token based on the redirect state value. - Store the mapping of tenant_id and unique state. - wait Microsoft support wildcard redirects.
State is for preventing CSRF, not transferring data. Don't abuse state, it's wrong.
Use your own authorize url, add an encrypted cookie and redirect to the real one. Even if the cookie is encrypted, only put some kind of session/cache key in it, don't actually send "info". Read cookie in callback then delete it.
"...In the process of disclosing and patching this vulnerability, the Harvest team was barely responsive. The company acknowledged the vulnerability by triaging but took a very long time to fix the vulnerability. After 3 years of reporting, the company finally fixed the vulnerability silently and didn't bother to inform...no bounty or even HackerOne points were rewarded by the company..."
And from the company page..
https://www.getharvest.com/security
"...Harvest cares deeply about protecting the privacy of the data entrusted to us by our customers. This is one of the core values at the heart of our business... "
Lol
The issue stayed on Triage state and I missed the reporter updates. I talked to the author of the post and I believe we are in good terms now.
The security and privacy of our customers is extremely important to us, everything we say in our security page is true and I've been working on this for years.