SAP's Gigya Trusted Domains Meets Domain Takeovers

I encountered a common misconfiguration when applications are using the Web SDK of Gigya to manage user identities across their application, which usually leads to a client-side account takeover. SAP's Customer Data Cloud (or Gigya) is an identity and access management platform, which allows customers to have authentication and authorization features implemented across their application. This is not vulnerability in SAP's Gigya platform, but rather describes a potential misconfiguration by its customers.

Web SDK

SAP offers their customers several ways to implement Gigya in their application. One of the options is the Web SDK. By embedding SAP's Gigya JavaScript SDK in your application, you can use Gigya's API within the authenticated user's context (or authenticate them if they are not logged in yet).

The Web SDK calls Gigya's API uses the user's Gigya SSO token to craft API calls. An example is /accounts.getAccountInfo to retrieve the user's account information. It's also possible to build registration/login flows using the Web SDK.

Trusted Domains

Gigya allows the customer to define trusted domains. From the defined domains by the customer, SAP's Gigya could be called within the authenticated user's context (and therefore retrieve or change any user information, add social logins, etc), if the user was authenticated via the Web SDK (as the token should be stored in the user's localStorage on the cdns.gigya.com origin).

SAP's Gigya Web SDK mainly works through JavaScript callbacks. If the client-side validation of trusted domains is passed, the Web SDK will craft the relevant API calls after retrieving the user's Gigya SSO token from localStorage (it does so after the SDK is loaded, by framing /gs/sso.htm on cdns.gigya.com. It communicates back the SSO token if it passes validation over postMessage).

 e.prototype.isValidDomain = function(e, t) {
// Uri.parse is a custom function, which creates an anchor element to parse the URL
                    for (var i = n.Uri.parse(t).domain, o = e.concat(this._defaultValidDomains), r = 0; r < o.length; r++) {
                        var a = o[r].replace("*", "").split(":")[0]
                          , s = i.indexOf(a);
                        if (s >= 0 && s === i.length - a.length && (0 === s || "." === i.charAt(s - 1) || "/" === i.charAt(s - 1)))
                            return !0
                    }
                    return !1
                }

The implemented validation seems to loop through the list of trusted domains (and adds some default domains like localhost before doing so), and uses indexOf to check whether the current hostname is in any of the trusted domains. It ensures we can not bypass the validation, as it validates whether the trusted domain is the last part of the current hostname (given it also validates the . is on the right place with charAt, we also can not bypass this validation via hostnames like evilgoogle.com, if a trusted domain is google.com).

It's interesting to note however that this validation allows the subdomains of any of the trusted domains.

Meeting (sub)-domain takeovers

I noticed that customers of SAP's Gigya Web SDK appear to be having an extensive list of trusted domains in many cases. Likely because they are sharing the same Gigya instance across different instances (production, staging, etc).

If it's possible to takeover one of the defined trusted domains (e.g. registering an expired domain, through subdomain takeover or otherwise) or its subdomains, you could host your own content and call SAP's Gigya API within the context of the authenticated user who is visiting your page.

From my own experience in reporting this issue to bug bounty programs, the severity of the issue usually aligns with a reflected cross-site scripting vulnerability, given the similar attack scenario.

Retrieving the list of trusted domains

You can get the list of trusted domains by reading the response from the SSO page (which is internally used by the Web SDK): https://cdns.gigya.com/gs/sso.htm?apikey=<GIGYA_API_KEY>. In the response, the _validDomains JavaScript variable will contain the trusted domains of the Gigya tenant.

...
var _validDomains = 'gigya.com,anonymous.persgroep.cloud,business.dk,persgroep.net'
var _logoutURLs = {"3_96-H":"https://example.com/gigya/logout"};
var _canaryCookiesNames = {"isCanary":"gig_canary","version":"gig_canary_ver"};
//end server injected code
...

Gigya Web SDK calls

If you were able to control any of the trusted domains (or its subdomains) defined by the customer, you could call the Gigya API within the authenticated user's context.

In the following example, whenever the victim visits your page as an authenticated user to the application, their SSO token will be printed on the page:

<script src="https://cdns.gigya.com/JS/gigya.js?apiKey=<api-key-of-app>"></script>
<script>
function getAccountInfo() { 
    gigya.accounts.getAccountInfo({
        "callback": function(res) {
            if (res.errorCode != 0) {
                console.log("error");   
            }
            console.log(res);
        }
    });
}
var onGigyaServiceReady = function() {
    getAccountInfo();
    document.write(window.gigya.auth.loginToken.get());
}
</script>