Revoking user session in https://hackerone.com/settings/sessions does not revoke the GraphQL query session
State Resolved (Closed)
Disclosed publicly 2018-11-30T19:21:17.498Z
Reported To
Weakness Insufficient Session Expiration
Bounty $500
Collapse


Timeline
submitted a report to HackerOne .
2018-10-02T02:24:25.196Z

Hi Team,

Summary:

I have found an Insufficient Session Expiration on implementation of the new Revoke user session feature of HackerOne here: https://hackerone.com/settings/sessions

Description:

The new REVOKE session feature will destroy the session of the selected device, that means any request that requires authorization should not work (POST, GET) BUT i have observed that you forgot the GraphQL query request, i can still query sensitive information despite i already revoke my current session and this can result a sensitive information disclosure

Steps To Reproduce

  1. Login to hackerone.com
  2. Go to sensitive page and capture the GraphQL request , on my case i captured the bounty settings {"query":"query User_bounty_settings_page... etc on this end point: https://hackerone.com/settings/bounties
  3. Send the GraphQL request to repeater
  4. Go to https://hackerone.com/settings/sessions and click Revoke to destroy the current session
  5. You will be logout to your account, means session destroyed
  6. Go back to burp repeater and click Go to repeat the graphql request
  7. Observed that you can still query the graphql User_bounty_settings_page, and it will disclosed the reports that was rewarded, including the amounts, report title, payment method used and other sensitive information.

Below is my sample GraphQL query and response after revoking the session:

{"query":"query User_bounty_settings_page($first_0:Int!,$currency_1:CurrencyCode!,$currency_2:CurrencyCode!) {\n  me {\n    id,\n    ...Fg\n  }\n}\nfragment F0 on PayoutPreferenceInterface {\n  default,\n  id,\n  __typename\n}\nfragment F1 on Node {\n  id,\n  __typename\n}\nfragment F2 on User {\n  tax_form {\n    url,\n    hello_sign_client_id,\n    status,\n    id\n  },\n  email,\n  bounties {\n    total_count\n  },\n  payout_preferences {\n    __typename,\n    ...F0,\n    ...F1\n  },\n  id\n}\nfragment F3 on CoinbasePayoutPreferenceType {\n  email,\n  id\n}\nfragment F4 on PaypalPayoutPreferenceType {\n  email,\n  id\n}\nfragment F5 on HackeronePayrollPayoutPreferenceType {\n  email,\n  id\n}\nfragment F6 on CurrencycloudBankTransferPayoutPreferenceType {\n  name,\n  id\n}\nfragment F7 on User {\n  payout_preferences {\n    __typename,\n    ...F0,\n    ...F3,\n    ...F4,\n    ...F5,\n    ...F6,\n    ...F1\n  },\n  id\n}\nfragment F8 on User {\n  _bounties4glIUB:bounties(first:$first_0) {\n    edges {\n      node {\n        id,\n        _id,\n        awarded_amount,\n        bonus_amount,\n        awarded_currency,\n        created_at,\n        status,\n        report {\n          _id,\n          title,\n          team {\n            name,\n            handle,\n            id\n          },\n          id\n        }\n      },\n      cursor\n    },\n    pageInfo {\n      hasNextPage,\n      hasPreviousPage\n    }\n  },\n  _bounties1CaoNY:bounties(currency:$currency_1) {\n    total_amount\n  },\n  id\n}\nfragment F9 on User {\n  _report_retest_usersixO8i:report_retest_users(completed:true,last:$first_0) {\n    completed_count,\n    edges {\n      node {\n        id,\n        completed_at,\n        report_retest {\n          report {\n            _id,\n            team {\n              handle,\n              name,\n              id\n            },\n            id\n          },\n          id\n        }\n      },\n      cursor\n    },\n    pageInfo {\n      hasNextPage,\n      hasPreviousPage\n    }\n  },\n  id\n}\nfragment Fa on User {\n  tax_form {\n    url,\n    status,\n    signed_at,\n    type,\n    id\n  },\n  id\n}\nfragment Fb on User {\n  id\n}\nfragment Fc on User {\n  _bounties3jJC5o:bounties(currency:$currency_2) {\n    total_amount\n  },\n  lufthansa_account {\n    id\n  },\n  id,\n  ...Fb\n}\nfragment Fd on User {\n  tax_form {\n    id\n  },\n  id,\n  ...Fb\n}\nfragment Fe on User {\n  lufthansa_account {\n    first_name,\n    last_name,\n    number,\n    id\n  },\n  id,\n  ...Fb\n}\nfragment Ff on User {\n  id,\n  ...Fb\n}\nfragment Fg on User {\n  id,\n  tax_form {\n    url,\n    id\n  },\n  ...F2,\n  ...F7,\n  ...F8,\n  ...F9,\n  ...Fa,\n  ...Fc,\n  ...Fd,\n  ...Fe,\n  ...Ff\n}","variables":{"first_0":100,"currency_1":"USD","currency_2":"XLA

Response:

HTTP/1.1 200 OK
Date: Tue, 02 Oct 2018 02:01:20 GMT
Content-Type: application/json; charset=utf-8
Connection: close
Cache-Control: no-cache, no-store
Content-Disposition: inline; filename="response."
X-Request-Id: 5ffd271a-bccd-4105-8991-4ed97769b1a0
Set-Cookie: ███
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Expect-CT: enforce, max-age=86400
Content-Security-Policy: default-src 'none'; base-uri 'self'; child-src www.youtube-nocookie.com b5s.hackerone-ext-content.com; connect-src 'self' www.google-analytics.com errors.hackerone.net; font-src 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data: cover-photos.hackerone-user-content.com hackathon-photos.hackerone-user-content.com profile-photos.hackerone-user-content.com hackerone-us-west-2-production-attachments.s3-us-west-2.amazonaws.com; media-src 'self' hackerone-us-west-2-production-attachments.s3-us-west-2.amazonaws.com; script-src 'self' www.google-analytics.com; style-src 'self' 'unsafe-inline'; report-uri https://errors.hackerone.net/api/30/csp-report/?sentry_key=61c1e2f50d21487c97a071737701f598
Referrer-Policy: strict-origin-when-cross-origin
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Frame-Options: DENY
X-Permitted-Cross-Domain-Policies: none
X-XSS-Protection: 1; mode=block
Server: cloudflare
CF-RAY: 4633948f1bbba2f6-HKG
Content-Length: 24732

████

PoC screenshot below:

█████

Impact

Insufficient Session Expiration can result to sensitive information disclosure.

Let me know if anything else is needed.

Regards
Japz

Regards,
Frans

  • 0 attachments:
japz Activities::Comment
2018-10-02T02:26:06.656Z
Revoking the user session should also immediately revoke the `GraphQL` session.


japz Activities::Comment
2018-10-02T02:45:55.896Z
Please note that i can perform any `POST` request using `GraphQL` despite i already __revoke__ the session. Below is another sample GraphQL `POST` request that i sent to update my invitation preference even __i already REVOKE my current session and i am already logout to the account__: ``` POST /graphql HTTP/1.1 Host: hackerone.com User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0 Accept: */* Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Referer: https://hackerone.com/settings/preferences content-type: application/json x-auth-token: █████████ origin: https://hackerone.com Content-Length: 1852 Cookie: █████████ Connection: close {"query":"mutation Update_invitation_preferences_mutation($input_0:UpdateInvitationPreferencesInput!,$first_1:Int!,$throttle_time_2:Int!,$size_3:ProfilePictureSizes!,$types_4:[ErrorTypeEnum]!) {\n updateInvitationPreferences(input:$input_0) {\n clientMutationId,\n ...F1,\n ...F2\n }\n}\nfragment F0 on User {\n id,\n i_can_update_invitation_profile,\n invitation_preference,\n hacker_invitations_profile {\n receive_invites,\n bounty_programs_only,\n managed_programs_only,\n min_bounty,\n id\n },\n bounties {\n average_amount\n },\n username,\n name,\n _profile_picturePkPpF:profile_picture(size:$size_3)\n}\nfragment F1 on UpdateInvitationPreferencesPayload {\n me {\n id,\n new_feature_notification {\n name,\n description,\n url,\n id\n },\n _program_health_acknowledgements2aGZgn:program_health_acknowledgements(first:$first_1,throttle_time:$throttle_time_2) {\n edges {\n node {\n id,\n reason,\n team_member {\n user {\n id\n },\n id,\n team {\n handle,\n name,\n sla_failed_count,\n id\n }\n }\n },\n cursor\n },\n pageInfo {\n hasNextPage,\n hasPreviousPage\n }\n },\n ...F0\n }\n}\nfragment F2 on UpdateInvitationPreferencesPayload {\n was_successful,\n _errors2S3JlH:errors(types:$types_4) {\n edges {\n node {\n type,\n field,\n message,\n id\n },\n cursor\n },\n pageInfo {\n hasNextPage,\n hasPreviousPage\n }\n }\n}","variables":{"input_0":{"invitation_preference":"bounty_only","clientMutationId":"1"},"first_1":1,"throttle_time_2":3600,"size_3":"small","types_4":"ARGUMENT"}} ``` __Below is the response:__ ``` HTTP/1.1 200 OK Date: Tue, 02 Oct 2018 02:42:09 GMT Content-Type: application/json; charset=utf-8 Connection: close Cache-Control: no-cache, no-store Content-Disposition: inline; filename="response." X-Request-Id: 1d213563-e247-408f-89f2-97a534478b36 Set-Cookie: ██████ Strict-Transport-Security: max-age=31536000; includeSubDomains; preload Expect-CT: enforce, max-age=86400 Content-Security-Policy: default-src 'none'; base-uri 'self'; child-src www.youtube-nocookie.com b5s.hackerone-ext-content.com; connect-src 'self' www.google-analytics.com errors.hackerone.net; font-src 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data: cover-photos.hackerone-user-content.com hackathon-photos.hackerone-user-content.com profile-photos.hackerone-user-content.com hackerone-us-west-2-production-attachments.s3-us-west-2.amazonaws.com; media-src 'self' hackerone-us-west-2-production-attachments.s3-us-west-2.amazonaws.com; script-src 'self' www.google-analytics.com; style-src 'self' 'unsafe-inline'; report-uri https://errors.hackerone.net/api/30/csp-report/?sentry_key=61c1e2f50d21487c97a071737701f598 Referrer-Policy: strict-origin-when-cross-origin X-Content-Type-Options: nosniff X-Download-Options: noopen X-Frame-Options: DENY X-Permitted-Cross-Domain-Policies: none X-XSS-Protection: 1; mode=block Server: cloudflare CF-RAY: 4633d060aa7aa2cc-HKG Content-Length: 693 {"data":{"updateInvitationPreferences":{"clientMutationId":"1","was_successful":true,"_errors2S3JlH":{"edges":[],"pageInfo":{"hasNextPage":false,"hasPreviousPage":false}},"me":{"id":"Z2lkOi8vaGFja2Vyb25lL1VzZXIvNzgzNDc=","new_feature_notification":null,"_program_health_acknowledgements2aGZgn":{"edges":[],"pageInfo":{"hasNextPage":false,"hasPreviousPage":false}},"i_can_update_invitation_profile":false,"invitation_preference":"bounty_only","hacker_invitations_profile":null,"bounties":{"average_amount":███},"username":"japz","name":"","_profile_picturePkPpF":"https://profile-photos.hackerone-user-content.com/000/078/347/456a23147b18f17e56cb5835da0ab06579e65910_small.png?1529606078"}}}} ``` Regards Japz


jobert Activities::ReportSeverityUpdated
2018-10-03T18:45:52.885Z


jobert Activities::BugTriaged
2018-10-03T18:53:35.472Z
Hi @japz - thanks for your report! This seems to something we missed on our side. I wanted to give you some background: the root cause of this problem comes from the fact that the GraphQL tokens are tied to a `User` object, not the individual sessions. This essentially means that all sessions use the same underlying secret token to generate a valid authentication token for GraphQL. We revoke this token when you'd sign out. The reason why this was never a problem was because we'd sign out ALL sessions. If we were to regenerate the token on revocation, all the other sessions would have to refresh their page, which is not a great UX. The solution to this would be to move the GraphQL secret token to the sessions OR deprecate the current authentication strategy (token) and implement OAuth (this would be better, although requires more time). I also wanted to point out two other things: 1. The GraphQL token can only be refreshed with a valid sessions; because this is revoked, an attacker wouldn't be able to obtain a new GraphQL token when they have a user's GraphQL token 2. A GraphQL token is only valid for 15 minutes; this means that the attacker needs to extract all the information or call the mutations within 15 minutes after the session was revoked We're going ahead and triage this. Given the above, this one might be open for some time. It is not a very likely exploit scenario, but we want the revocation to be an actual revocation of the entire session, not a partial one. We were also not aware of this, so we want to thank you for that by triaging this and awarding you with a bounty. Thanks, good luck, and happy hacking!


japz Activities::Comment
2018-10-04T01:27:23.991Z
Hi @jobert Thanks for giving me a very detailed background regarding the GraphQL token, much appreciated :) >The solution to this would be to move the GraphQL secret token to the sessions OR deprecate the current authentication strategy (token) and implement OAuth (this would be better, although requires more time). I agree, moving the GraphQL secret token to sessions was good.. but no worries if it requires more time for the other option.. it's all up to you if what solution would you like to implement. >I also wanted to point out two other things. I agree with the 2 items pointed out above, but i just want to remind you that 15 mins (graphql session expiration) is enough time for the attacker to extract sensitive information as well as calling `POST` request GraphQL mutations for a critical endpoint such as updating payment method etc. >We're going ahead and triage this. Given the above, this one might be open for some time. Cool.. no worries, take your time. >We were also not aware of this, so we want to thank you for that by triaging this and awarding you with a bounty. Sounds good, I'm glad i was able to help. :) Cheers! Japz


bigbug Activities::ExternalUserJoined
2018-10-09T20:04:54.147Z


indo_heker Activities::ExternalUserJoined
2018-10-10T16:53:40.009Z


indo_heker Activities::ExternalUserRemoved
2018-10-10T17:16:04.748Z


bigbug Activities::Comment
2018-10-10T17:39:27.198Z
@jobert - gentle reminder again. Consider redacting sensitive information provided by @japz in his POC response. This contains his payment details, reports etc. As this report is attracting more external hackers, I feel the info must be redacted. I believe @japz would want this too.


japz Activities::Comment
2018-10-11T05:23:35.100Z
@jobert - Can you redact the __response__ in my initial description for this GraphQL query name: `User_bounty_settings_page` ? Because the response in my PoC contains some sensitive private information such as report `title`, `bounty amount`, `private program handle` as well as my `email address`. @bigbug thanks for the heads up. Regards Japz


jobert Activities::Comment
2018-10-11T13:04:17.551Z
Hi @japz - all done! I initially only redacted the `Set-Cookie` header. Let me know if there's anything else that needs to be redacted. Doesn't look like it, but happy to remove more if you want.


japz Activities::Comment
2018-10-11T13:14:53.782Z
Hi @jobert Looks good now. Thank you as always.. much appreciated :) Regads Japz


Activities::BountyAwarded
2018-10-12T17:16:05.602Z
Thanks for the awesome find! We'll still be working on fixing the issue, but in the mean time, here's your bounty 💰💰💰


japz Activities::Comment
2018-10-13T04:04:11.993Z
Hi @bencode First, thank you so much for the bounty.. btw just want to ask if this is initial bounty or a full bounty ? Because based on the CVSS score, my submission is Low `3.9`, only `.1` remaining point and it become a `Medium` severity. but still the reward is minimum of a Low severity submission, so i was wondering if this bounty is base on the `3.9` CVSS score ? Regards


jobert Activities::Comment
2018-10-14T18:53:28.926Z
Hi @japz - we generally default to the amount that we pay for the severity bracket. There have been exceptions, either for creativity, the writeup, the involved effort, or uniqueness. In this case, we'll keep it at $500. Thanks for checking!


japz Activities::Comment
2018-10-15T01:09:15.721Z
@jobert - I see, thanks for letting me know. Regards


gxlf61g6dy Activities::ExternalUserJoined
2018-10-29T23:15:44.264Z


gxlf61g6dy Activities::Comment
2018-10-30T06:01:50.777Z
Haha, Thanks you @jobert for making 2 of my reports in duplicate for this report xD although it doesn't click revoke Session, but this is a new lesson;)) so I have to go to the point in next report. and I like your spirit @japz Cheers, @indo_heker


japz Activities::Comment
2018-11-19T02:28:56.056Z
Hi @jobert , Just visiting this report, It's been 2 months.. do we have updates on this ? Regards Japz


jobert Activities::Comment
2018-11-20T05:26:28.905Z
Hi @japz - it'll likely be open for another few weeks. The task has been prioritized and will be picked up by someone soon. We'll keep you posted on our progress. Good luck and happy hacking!


jobert Activities::BugResolved
2018-11-27T16:29:42.147Z
Hi @japz - earlier today, a fix was released for the security vulnerability you reported! We can go ahead and close this out now. Thanks again, it's much appreciated. Good luck and happy hacking!


jobert Activities::ExternalUserInvited
2018-11-27T16:31:07.126Z
None


jobert Activities::ExternalUserInvited
2018-11-27T16:31:07.871Z
None


jobert Activities::ExternalUserInvited
2018-11-27T16:31:08.663Z
None


jobert Activities::ExternalUserInvited
2018-11-27T16:31:09.288Z
None


teknogeek Activities::ExternalUserJoined
2018-11-27T17:15:10.895Z


borski Activities::ExternalUserJoined
2018-11-27T18:45:29.623Z


wang_johnny Activities::ExternalUserJoined
2018-11-27T22:37:17.318Z


japz Activities::AgreedOnGoingPublic
2018-11-28T01:43:13.137Z
@jobert - You can pull the trigger once all the retesters are done on their verification. Cheers! Japz


bull Activities::ExternalUserJoined
2018-11-28T07:13:44.701Z


jobert Activities::AgreedOnGoingPublic
2018-11-30T19:21:17.394Z
Thanks everybody for completing the retest of this report. No bypasses or regressions were found. All sensitive information in this report has been cleaned up, so this is ready to be disclosed. Happy hacking all!


jobert Activities::ReportBecamePublic
2018-11-30T19:21:17.524Z