HexBrain

HackerOne · Report #489146

Confidential data of users and limited metadata of programs and reports accessible via GraphQL

Validstatus
criticalseverity
1,033upvotes
3 Feb 2019disclosed

Summary

On January 31st, 2019 at 7:16pm PST, HackerOne confirmed that two reporters were able to query confidential data through a GraphQL endpoint. This vulnerability was introduced on December 17th, 2018 and was caused by a backend migration to a class-based implementation of GraphQL types, mutations, and connections. The class-based implementation introduced the nodes field by default on all connections. The nodes field, in contrast with edges, didn’t leverage any of the defenses HackerOne has implemented to mitigate the exposure of sensitive information.

Our investigation concluded that malicious actors did not exploit the vulnerability. No confidential data was compromised. A short-term fix was released on January 31st, 2019 at 9:46 PM, a little over 2 hours after the vulnerability was reproduced.

Timeline

DateTime (PST)Action
2018-12-179:07 AMSoftware containing bug deployed to production.
2019-01-317:32 AMVulnerability submitted to HackerOne’s bug bounty program.
2019-01-317:21 PMHackerOne validated the report and started incident response.
2019-01-318:25 PMHackerOne identified which code change introduced the security vulnerability and started work on a patch.
2019-01-319:46 PMA patch was released mitigating the identified vulnerability.
2019-01-3111:46 PMHackerOne confirmed the vulnerability was not abused by any malicious actors.
2019-02-016:18 AMThe root cause of the vulnerability was identified and a long term mitigation was proposed.
2019-02-015:08 PMLong term mitigation was deployed to production.
2019-02-032:34 AMImpacted users were alerted that their information was exposed to the reporters who submitted the vulnerability.

Root Cause

HackerOne has a number of defenses in place to reduce the risk of over-exposing data through our GraphQL layer. The first notable defense is a separate database schema that limits the set of rows a user can query based on their current role. This significantly reduces the impact in case, for example, the result of Report.all, would be serialized and returned to the user. The second notable defense is attribute-level authorization depending on the role of the requester. This makes sure that when an object is serialized, for example a publicly disclosed report, the user is not able to obtain internal metadata of the report.

Why upgrade? On December 17th, when the code change was put up for review, engineers noticed the addition of the nodes field. An assumption was made that the field behaved like a shortcut for edges { node } — which, in hindsight, was not the case. No manual testing was performed to make sure that the authorization model for nodes was similar to other connection types.

HackerOne’s engineering team decided to upgrade to the class-based implementation of graphql-ruby because the old .define-based implementation was lazy-loaded. This caused problems when hot reloading pieces of code in a development environment. The class-based implementation also performs better in most situations. The .define-style implementation is also deprecated by the maintainers of the gem (to be removed with GraphQL 2.0).

Why didn’t we notice? The nodes field is a helper field for Relay, which is used by the frontend. Even though the field was introduced, HackerOne engineers hadn’t started using this in our frontend. This caused the addition to fly under the radar of other engineers. The go-to way to query data through connection types at HackerOne is to go through the edges field. Because engineers outside of the specific team who upgraded to the class-based implementation did not deem the change important enough, there was no communication to other engineering teams.

Why was it exploitable? When a GraphQL query is deconstructed and turned into one or multiple SQL queries, it will cast the result of it into an array of stale objects and use the attribute-level authorization to scrub all data the current user isn’t authorized to see. Root cause analysis showed that this code path was only followed when the nodes were queried through the edges field.

Query that followed the expected code path

query {
  users() {
    edges {
      node {
        email
      }
    }
  }
}

During the GraphQL gem upgrade on December 17th, all GraphQL types, connections, and mutations were rewritten to a class-based implementation. This introduced the nodes field on every connection type in HackerOne’s GraphQL schema. Instead of casting the result to an array with stale objects, the nodes field would result in an ActiveRecord::Relation object. The attribute-level authorization instrumentation would then incorrectly assume that the result was safe to be serialized, as it assumes the parent of the GraphQL field had already been scrubbed.

Query that followed the unexpected code path

query {
  users() {
    nodes {
      email
    }
  }
}

In the team’s investigation to determine whether this was exploited by malicious actors, the team concluded that the current logging level enabled them to answer two crucial questions: which GraphQL queries were executed and what information was transferred to the people proving the security vulnerability in the first place. These questions confirmed it was not exploited.

Resolution and Recovery

At 7:21 PM PST, HackerOne successfully reproduced the vulnerability as described by the reporter. The responding team identified the code change that introduced the vulnerability and started working on a short-term mitigation at 8:25 PM. This mitigation was released at 9:46 PM. The short-term mitigation was to disable the nodes field from every connection type. An internal code rule was deployed to alert the incident responders in case a new connection type was added that had the nodes field enabled. At the time, the root cause of the vulnerability was still unclear.

On February 1st at 6:18 AM, the team concluded the root cause analysis of the identified vulnerability. A long-term fix was put up for discussion. This fix addressed the underlying problem of the lack of attribute-level protection for the nodes field. Going forward any connection type that is introduced will either be sanitized through the attribute-level authorization or will stop processing the request in case of an unexpected object to be returned.

The minimum bounty award for a critical vulnerability on hackerone.com is currently set to $15,000. Even though this vulnerability exposed confidential information, it was limited to user information and metadata of programs and reports. None of the exposed information could have led to the compromise of confidential vulnerability information. It did, however, allow actors to query a significant amount of information. Because of that, the team decided to award the reporters with $20,000 for uncovering this vulnerability and working with us throughout the process.

Vulnerability Impact on Data

Sensitive information of multiple objects was exposed. Due to the two notable defenses as described in the Root Cause section, the scope of the information that was exposed was limited. Below is an overview of the objects and the confidential data that a user was able to access.

Connection: users The GraphQL schema enables anyone to query the users on the platform. This is an intentional design decision. However, because every User object could be accessed, a significant amount of confidential information was accessible.

Below is an overview of all sensitive attributes that could be queried for every user on hackerone.com.

Sensitive attributeNote
account_recovery_phone_numberThe last two digits of a verified account recovery phone number.
account_recovery_unverified_phone_numberThe complete unverified account recovery phone number.
addressAccessible when swag was awarded for a report the authenticated user had access to, regardless of their role (e.g. publicly disclosed report).
calendar_tokenThe secret calendar token that exposes when HackerOne challenges were scheduled for the user. This does not expose customer names.
duplicate_usersAn array of possible duplicate accounts based on platform behavior.
emailThe email address.
otp_backup_codesAn array of bcrypt-hashed OTP backup codes.
payout_preferencesA connection of the user’s payout preferences. This does not include bank account details.
reportsSee Report connection for the scope and attributes that were exposed.
unconfirmed_emailThe unconfirmed email address.

Connection: teams The secure database schema, by default, allows any user to query public programs (teams) and public external programs. Because of the relationship between external programs and HackerOne programs, this data set includes programs who may be running a private program. This means it was possible to obtain internal triage notes and the policy of a select number of private programs the user did not have access to. The reporters queried partial program information, but they did not obtain any sensitive information that warranted HackerOne to reach out to any customers.

Sensitive attributeNote
average_bounty_lower_amountThe lower bound of the average bounty range.
average_bounty_upper_amountThe higher bound of the average bounty range.
base_bountyThe minimum bounty of a program.
bounties_totalThe sum of awarded bounties in the entire lifetime of the program.
bug_countThe total number of resolved reports.
child_teamsA connection containing the hierarchy of teams.
first_response_timeA float containing the average time to first response.
goal_valid_reportsThe goal of valid vulnerabilities per month the program set.
grace_period_remaining_in_daysThe number of days the program has to recover from too many SLA failures to avoid their program being taken off HackerOne.
new_staleness_thresholdThe internal SLA until a report is marked as an SLA miss when it hasn’t received a first response.
new_staleness_threshold_limitThe internal SLA until a report is marked as an SLA fail when it hasn’t received a first response.
policyThe program policy in raw markdown.
policy_htmlThe rendered program policy.
product_editionThe product edition the program uses.
report_submission_form_introThe submission form introduction in raw markdown.
report_submission_form_intro_htmlThe rendered submission form introduction.
report_templateThe default report template in raw markdown.
reportersAn array of user objects who have reporter access to the program.
resolution_timeA float containing the average time to resolution.
resolved_staleness_thresholdThe internal SLA until a report is marked as an SLA miss when it hasn’t been resolved.
sla_failed_countThe number of reports failing the internal SLA.
structured_policyA structured representation of the program policy.
structured_scopesA connection that only disclosed an internal reference in case the user was authorized to see the structured scopes on the program page.
target_signalA float representing the targeted signal of the program.
triage_bounty_managementA text field containing instructions for HackerOne’s triage team on how to handle bounty payments.
triage_enabledA boolean field indicating whether the program uses HackerOne’s triage services.
triage_noteInternal triage notes in raw markdown.
triage_note_htmlThe rendered triage notes.
triage_timeA float containing the average time to triage.
triaged_staleness_thresholdThe internal SLA until a report is marked as an SLA miss when it hasn’t been triaged.
triaged_staleness_threshold_limitThe internal SLA until a report is marked as an SLA fail when it hasn’t been triaged.
whitelisted_hackersSee reporters.

Connection: reports The reports data hasn’t been fully migrated to the secure database schema yet, which means that at the time the vulnerability was reported, only fully publicly disclosed and all reports the user participated in were accessible. This significantly reduced the number of report information that was exposed.

Sensitive attributeNote
anc_reasonsAn array of strings containing flags why the report was submitted to the HackerOne Human-Augmented Signal queue.
mediation_requested_atA date/time field when mediation was requested.
pre_submission_review_stateA flag representing how Human-Augmented Signal responded to the report.
referenceAn optional internal reference.
reference_linkAn optional link to an internal ticket.

Even though the reporters confirmed that they did not query more information than necessary to prove the vulnerability and that they have deleted the information, HackerOne has reached out to the people for which sensitive information was downloaded by the reporters.

If your data was accessed during this incident, you have received a separate notification from HackerOne.

Preventative Measures

As part of our incident response process, we are conducting an internal review and analysis of the incident. We are taking the following actions to address the underlying causes of issues and to help prevent future occurrence:

  • Consider leveraging the graphql-ruby gem hooks for built-in authorization callbacks to catch more edge cases
  • Break the execution flow when an unexpected object is returned in the resolution of a connection field
  • Reduce the complexity of connection type resolution

How it was found

Summary: The GraphQL endpoint doesn't have access controls implemented properly.

Description: Any attacker can get personally identifiable information of users of Hackerone such as email address, backup hash codes, facebook_user_id, account_recovery_phone_number_verified_at, totp_enabled, etc.

These are just some examples of fields which are getting leaked directly from GraphQL.

This is the request sent to GraphQL:

{
  id
  users()
  {
    total_count 
    nodes
    {
      _id
      name
      username
      email
      account_recovery_phone_number
      account_recovery_unverified_phone_number
      bounties
      {
        total_amount
      }
      otp_backup_codes
      i_can_update_username
      location
      year_in_review_published_at
      anc_triager
      blacklisted_from_hacker_publish
      calendar_token
      vpn_credentials
      {
        name
      }
      account_recovery_phone_number_sent_at
      account_recovery_phone_number_verified_at
      swag
      {
        total_count
      }
      totp_enabled
      subscribed_for_team_messages
      subscribed_for_monthly_digest
      sessions
      {
        total_count
      }
      facebook_user_id
      unconfirmed_email
    }
  }

Sample Response: █████████

Please fix it.

Thanks, Yash :)

Impact

This could potentially leak many users' info

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original