Finding hidden gems vol. 1: forging OAuth tokens using discovered client id and client secret

I love sensitive information exposure bugs. They are getting more attention at last. Below a short story about leaked Node.js code and OAuth client id and client secret which I found in there.


One of my bug bounty recon tools discovered a package.json file which looked interesing. The package.json file is a description of a Node.js module. This particular one looked like below:

"name": "[…redacted…]",
"description": "[…redacted…]",
"version": "[…redacted…]",
"private": true,
"engines": {
"node": ">=6.9.1"
"scripts": {
"start": "node server/[…redacted…].js"

Setting scripts contained a path to some Node.js code to execute. I was curious what’s in that file, therefore I immediately downloaded the content. It was a huge file, containing Node.js code.


I found there four variables which looked very promissing.

const devClientId = '11e7f9d3–9bbf-4a01-a23e-c9c58e3acb1d';
const prodClientId = 'a2ae2727-aa6a-4197–823e-40e6d4e503a7';
const devClientSecret = 'd[…redacted…]=';
const prodClientSecret = 'b[…redacted…]=';

Disclaimer: all values were replaced or redacted.

It ringed a distant bell related to one of the OAuth grant types — the client credentials. To quote the specs:

Client credentials are used as an authorization grant
typically when the client is acting on its own behalf (the client is
also the resource owner) or is requesting access to protected
resources based on an authorization previously arranged with the
authorization server.

From other recon results I knew that this company is built on top of Microsoft Azure therefore I searched for OAuth integration documentation. Now I just wanted to check if prodClientId and prodClientSecret are still valid:

POST /[redacted] HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 190
HTTP/1.1 200 OK

I got back a JWT token and just one thing left — to actually use it and try to get some data. I’ve selected an API call which returns users:

GET /v1.0/users HTTP/1.0
Authorization: Bearer eyJ[…redacted…]
HTTP/1.1 200 OK
“businessPhones”:[],”displayName”:”Stuart […redacted…]”,
“displayName”:”Ashley […redacted…],

It worked! This was found in a public program on Bugcrowd and they gave me 3133.7$ for this finding.


The company fixed it like below — now those variables are taken out of Consul:

class Consul {
constructor(url) {
this.consulHost = url;
getClientId() {
return this.getConsulValue(‘v1/[redacted]/CLIENT_ID’);
getClientSecret() {
return this.getConsulValue(‘v1/[redacted]/APP_KEY’);
getConsulValue(consulKey) {
return request({ url: `${this.consulHost}/${consulKey}`, json: true }).promise()
.then((res) => res[0].Value)
.then((val) => (new global.Buffer(val, ‘base64’)).toString(‘utf8’));
exports.Consul = Consul;

Where consulHost in taken from an environmental variable:

const consulHost = process.env.CONSUL

Lessons learned:

  • For myself — don’t give up, make a break, return to the problem next day and solve it easily.
  • Bug hunters — definitely check for package.json files.
  • Developers — check what is in your package.json file and make sure you don’t leak anything interesing.