CVE-2023-30846: Insufficiently Protected Credentials (CWE-522)
value: typed-rest-client is a library for Node Rest and Http Clients with typings for use with TypeScript. Users of the typed-rest-client library version 1.7.3 or lower are vulnerable to leak authentication data to 3rd parties. The flow of the vulnerability is as follows: First, send any request with `BasicCredentialHandler`, `BearerCredentialHandler` or `PersonalAccessTokenCredentialHandler`. Second, the target host may return a redirection (3xx), with a link to a second host. Third, the next request will use the credentials to authenticate with the second host, by setting the `Authorization` header. The expected behavior is that the next request will *NOT* set the `Authorization` header. The problem was fixed in version 1.8.0. There are no known workarounds.
CWE description: The product transmits or stores authentication credentials, but it uses an insecure method that is susceptible to unauthorized interception and/or retrieval.
CVSS v3 base score: 7.5
Published: 2023-04-26T21:15Z
Repository: https://github.com/microsoft/typed-rest-client
Language: TypeScript
Files changed: lib/handlers/basiccreds.ts, lib/handlers/bearertoken.ts, lib/handlers/personalaccesstoken.ts, test/units/handlers.ts
Commit date: 2020-04-01 09:08:10 -0400
Commit message: Disable authentications on redirections (#207)

* Disable authentications on redirections

* Add allowCrossOriginAuthentication boolean
Diff: diff --git a/lib/handlers/basiccreds.ts b/lib/handlers/basiccreds.ts
index d8f31f7..3730cdd 100644
--- a/lib/handlers/basiccreds.ts
+++ b/lib/handlers/basiccreds.ts
@@ -4,20 +4,29 @@
 import ifm = require('../Interfaces');
 
 export class BasicCredentialHandler implements ifm.IRequestHandler {
     username: string;
     password: string;
+    allowCrossOriginAuthentication: boolean;
+    origin: string;
 
-    constructor(username: string, password: string) {
+    constructor(username: string, password: string, allowCrossOriginAuthentication?: boolean) {
         this.username = username;
         this.password = password;
+        this.allowCrossOriginAuthentication = allowCrossOriginAuthentication;
     }
 
     // currently implements pre-authorization
     // TODO: support preAuth = false where it hooks on 401
     prepareRequest(options:any): void {
-        options.headers['Authorization'] = `Basic ${Buffer.from(`${this.username}:${this.password}`).toString('base64')}`;
+        if (!this.origin) {
+            this.origin = options.host;
+        }
+        // If this is a redirection, don't set the Authorization header
+        if (this.origin === options.host || this.allowCrossOriginAuthentication) {
+            options.headers['Authorization'] = `Basic ${Buffer.from(`${this.username}:${this.password}`).toString('base64')}`;
+        }
         options.headers['X-TFS-FedAuthRedirect'] = 'Suppress';
     }
 
     // This handler cannot handle 401
     canHandleAuthentication(response: ifm.IHttpClientResponse): boolean {
diff --git a/lib/handlers/bearertoken.ts b/lib/handlers/bearertoken.ts
index f7fad93..6672298 100644
--- a/lib/handlers/bearertoken.ts
+++ b/lib/handlers/bearertoken.ts
@@ -3,19 +3,28 @@
 
 import ifm = require('../Interfaces');
 
 export class BearerCredentialHandler implements ifm.IRequestHandler {
     token: string;
+    allowCrossOriginAuthentication: boolean;
+    origin: string;
 
-    constructor(token: string) {
+    constructor(token: string, allowCrossOriginAuthentication?: boolean) {
         this.token = token;
+        this.allowCrossOriginAuthentication = allowCrossOriginAuthentication;
     }
 
     // currently implements pre-authorization
     // TODO: support preAuth = false where it hooks on 401
     prepareRequest(options:any): void {
-        options.headers['Authorization'] = `Bearer ${this.token}`;
+        if (!this.origin) {
+            this.origin = options.host;
+        }
+        // If this is a redirection, don't set the Authorization header
+        if (this.origin === options.host || this.allowCrossOriginAuthentication) {
+            options.headers['Authorization'] = `Bearer ${this.token}`;
+        }
         options.headers['X-TFS-FedAuthRedirect'] = 'Suppress';
     }
 
     // This handler cannot handle 401
     canHandleAuthentication(response: ifm.IHttpClientResponse): boolean {
diff --git a/lib/handlers/personalaccesstoken.ts b/lib/handlers/personalaccesstoken.ts
index eab4687..3af85ea 100644
--- a/lib/handlers/personalaccesstoken.ts
+++ b/lib/handlers/personalaccesstoken.ts
@@ -3,19 +3,28 @@
 
 import ifm = require('../Interfaces');
 
 export class PersonalAccessTokenCredentialHandler implements ifm.IRequestHandler {
     token: string;
+    allowCrossOriginAuthentication: boolean;
+    origin: string;
 
-    constructor(token: string) {
+    constructor(token: string, allowCrossOriginAuthentication?: boolean) {
         this.token = token;
+        this.allowCrossOriginAuthentication = allowCrossOriginAuthentication;
     }
 
     // currently implements pre-authorization
     // TODO: support preAuth = false where it hooks on 401
     prepareRequest(options:any): void {
-        options.headers['Authorization'] = `Basic ${Buffer.from(`PAT:${this.token}`).toString('base64')}`;
+        if (!this.origin) {
+            this.origin = options.host;
+        }
+        // If this is a redirection, don't set the Authorization header
+        if (this.origin === options.host || this.allowCrossOriginAuthentication) {
+            options.headers['Authorization'] = `Basic ${Buffer.from(`PAT:${this.token}`).toString('base64')}`;
+        }
         options.headers['X-TFS-FedAuthRedirect'] = 'Suppress';
     }
 
     // This handler cannot handle 401
     canHandleAuthentication(response: ifm.IHttpClientResponse): boolean {
diff --git a/test/units/handlers.ts b/test/units/handlers.ts
index 495cbc3..99d1751 100644
--- a/test/units/handlers.ts
+++ b/test/units/handlers.ts
@@ -78,10 +78,54 @@ describe('Authentication Handlers Tests', function () {
         assert(httpResponse.message.statusCode == httpm.HttpCodes.Unauthorized, "status code should be 401 - Unauthorized");
         assert(asJson.source === "nock", "http get request should be intercepted by nock");
         assert(! asJson.success, "success = false; Authentication should fail");
     });
 
+    it('[Basic Auth] - does redirection request with basic auth', async() => {
+        const url: string = 'http://microsoft.com';
+        const redirectionUrl: string = 'http://jfrog.com';
+        const user: string = _authHandlersOptions.basicAuth.username;
+        const pass: string = _authHandlersOptions.basicAuth.password;
+
+        //Set nock for redirection with credentials
+        const redirectAuthScope = nock(url)
+            .get('/')
+            .basicAuth({ user, pass })
+            .reply(httpm.HttpCodes.MovedPermanently, undefined, {
+                location: redirectionUrl
+            });
+
+        //Set nock for request without expecting/matching Authorization header(s)
+        nock(redirectionUrl)
+            .matchHeader('authorization', (val: string | undefined) => !val )
+            .get('/')
+            .reply(httpm.HttpCodes.OK, {
+                success: true,
+                source: "nock"
+            });
+
+        //Set nock for request with expecting/matching Authorization header(s)
+        nock(redirectionUrl)
+            .matchHeader('authorization', (val: string | undefined) => val )
+            .get('/')
+            .reply(httpm.HttpCodes.BadRequest, {
+                success: false,
+                source: "nock"
+            });
+
+        const basicAuthHandler: hm.BasicCredentialHandler = new hm.BasicCredentialHandler(user, pass);
+        let httpClient: httpm.HttpClient = new httpm.HttpClient('typed-rest-client-tests', [basicAuthHandler]);
+        let httpResponse: httpm.HttpClientResponse = await httpClient.get(url);
+        let body: string = await httpResponse.readBody();
+        let asJson: any = JSON.parse(body);
+
+        assert(redirectAuthScope.isDone());
+        assert(httpResponse.message.statusCode == httpm.HttpCodes.OK, "status code should be 200 - OK");
+        assert(asJson.source === "nock", "http get request should be intercepted by nock");
+        assert(asJson.success, "Authentication should not occur in redirection to other hosts");
+    });
+
     it('[Basic Auth - Presigned] doesnt use auth when presigned', async() => {
         const url: string = 'http://microsoft.com';
         const user: string = _authHandlersOptions.basicAuth.username;
         const pass: string = _authHandlersOptions.basicAuth.password;
 
@@ -163,10 +207,57 @@ describe('Authentication Handlers Tests', function () {
         assert(httpResponse.message.statusCode == httpm.HttpCodes.Unauthorized, "status code should be 401 - Unauthorized");
         assert(asJson.source === "nock", "http get request should be intercepted by nock");
         assert(! asJson.success, "success = false; Authentication should fail");
     });
 
+    it('[Personal Access Token] - does redirection request with PAT token auth', async() => {
+        const url: string = 'http://microsoft.com';
+        const redirectionUrl: string = 'http://jfrog.com';
+        const secret: string = _authHandlersOptions.personalAccessToken.secret;
+        const personalAccessToken: string = Buffer.from(`PAT:${secret}`).toString('base64');
+        const expectedAuthHeader: string = `Basic ${personalAccessToken}`;
+        const patAuthHandler: hm.PersonalAccessTokenCredentialHandler =
+        new hm.PersonalAccessTokenCredentialHandler(secret);
+
+        //Nock request for redirection with expecting/matching Authorization header(s)
+        const redirectAuthScope = nock(url)
+            .matchHeader('Authorization', expectedAuthHeader)
+            .matchHeader('X-TFS-FedAuthRedirect', 'Suppress')
+            .get('/')
+            .reply(httpm.HttpCodes.MovedPermanently, undefined, {
+                location: redirectionUrl
+            });
+
+        //Set nock for request without expecting/matching Authorization header(s)
+        nock(redirectionUrl)
+            .matchHeader('authorization', (val: string | undefined) => !val )
+            .get('/')
+            .reply(httpm.HttpCodes.OK, {
+                success: true,
+                source: "nock"
+            });
+
+        //Set nock for request with expecting/matching Authorization header(s)
+        nock(redirectionUrl)
+            .matchHeader('authorization', (val: string | undefined) => val )
+            .get('/')
+            .reply(httpm.HttpCodes.BadRequest, {
+                success: false,
+                source: "nock"
+            });
+
+        let httpClient: httpm.HttpClient = new httpm.HttpClient('typed-rest-client-tests', [patAuthHandler]);
+        let httpResponse: httpm.HttpClientResponse = await httpClient.get(url);
+        let body: string = await httpResponse.readBody();
+        let asJson: any = JSON.parse(body);
+
+        assert(redirectAuthScope.isDone());
+        assert(httpResponse.message.statusCode == httpm.HttpCodes.OK, "status code should be 200 - OK");
+        assert(asJson.source === "nock", "http get request should be intercepted by nock");
+        assert(asJson.success, "Authentication should not occur in redirection to other hosts");
+    });
+
     it('[Bearer Token] - does basic http get request with bearer token authentication', async() => {
         const url: string = 'http://microsoft.com';
         const bearerToken: string = _authHandlersOptions.bearer.token;
 
         const expectedAuthHeader: string = `Bearer ${bearerToken}`;
@@ -214,10 +305,56 @@ describe('Authentication Handlers Tests', function () {
         assert(failureAuthScope.isDone(), "Nock Scope of failureAuth should be intercepted/done"); //nock test for failure auth is done
         assert(! responseBodyAsJSON.success, "Success should be set to false"); // success: false
         assert(httpResponse.message.statusCode === httpm.HttpCodes.Unauthorized, "statusCode returned should be 401 - Unauthorized"); //statusCode is 401 - Unauthorized
     });
 
+    it('[Bearer Token] - does redirection request with bearer token authentication', async() => {
+        const url: string = 'http://microsoft.com';
+        const redirectionUrl: string = 'http://jfrog.com';
+        const bearerToken: string = _authHandlersOptions.bearer.token;
+
+        const expectedAuthHeader: string = `Bearer ${bearerToken}`;
+        const bearerTokenAuthHandler: hm.BearerCredentialHandler = new hm.BearerCredentialHandler(bearerToken);
+
+        //Nock request for redirection with expecting/matching Authorization header(s)
+        const redirectAuthScope = nock(url)
+            .matchHeader('Authorization', expectedAuthHeader)
+            .matchHeader('X-TFS-FedAuthRedirect', 'Suppress')
+            .get('/')
+            .reply(httpm.HttpCodes.MovedPermanently, undefined, {
+                location: redirectionUrl
+            });
+
+        //Set nock for request without expecting/matching Authorization header(s)
+        nock(redirectionUrl)
+            .matchHeader('authorization', (val: string | undefined) => !val )
+            .get('/')
+            .reply(httpm.HttpCodes.OK, {
+                success: true,
+                source: "nock"
+            });
+
+        //Set nock for request with expecting/matching Authorization header(s)
+        nock(redirectionUrl)
+            .matchHeader('authorization', (val: string | undefined) => val )
+            .get('/')
+            .reply(httpm.HttpCodes.BadRequest, {
+                success: false,
+                source: "nock"
+            });
+
+        let httpClient: httpm.HttpClient = new httpm.HttpClient('typed-rest-client-tests', [bearerTokenAuthHandler]);
+        let httpResponse: httpm.HttpClientResponse = await httpClient.get(url);
+        let body: string = await httpResponse.readBody();
+        let asJson: any = JSON.parse(body);
+
+        assert(redirectAuthScope.isDone());
+        assert(httpResponse.message.statusCode == httpm.HttpCodes.OK, "status code should be 200 - OK");
+        assert(asJson.source === "nock", "http get request should be intercepted by nock");
+        assert(asJson.success, "Authentication should not occur in redirection to other hosts");
+    });
+
     it('[NTLM] - does basic http get request with NTLM Authentication', async() => {
         /**
          * Following NTLM Authentication Example on:
          * https://www.innovation.ch/personal/ronald/ntlm.html
          * With username: "Zaphod", password: "Beeblebrox" &

Vulnerable code:     constructor(username: string, password: string) {
        options.headers['Authorization'] = `Basic ${Buffer.from(`${this.username}:${this.password}`).toString('base64')}`;
    constructor(token: string) {
        options.headers['Authorization'] = `Bearer ${this.token}`;
    constructor(token: string) {
        options.headers['Authorization'] = `Basic ${Buffer.from(`PAT:${this.token}`).toString('base64')}`;
Fixed code:     allowCrossOriginAuthentication: boolean;
    origin: string;
    constructor(username: string, password: string, allowCrossOriginAuthentication?: boolean) {
        this.allowCrossOriginAuthentication = allowCrossOriginAuthentication;
        if (!this.origin) {
            this.origin = options.host;
        }
        // If this is a redirection, don't set the Authorization header
        if (this.origin === options.host || this.allowCrossOriginAuthentication) {
            options.headers['Authorization'] = `Basic ${Buffer.from(`${this.username}:${this.password}`).toString('base64')}`;
        }
    allowCrossOriginAuthentication: boolean;
    origin: string;
    constructor(token: string, allowCrossOriginAuthentication?: boolean) {
        this.allowCrossOriginAuthentication = allowCrossOriginAuthentication;
        if (!this.origin) {
            this.origin = options.host;
        }
        // If this is a redirection, don't set the Authorization header
        if (this.origin === options.host || this.allowCrossOriginAuthentication) {
            options.headers['Authorization'] = `Bearer ${this.token}`;
        }
    allowCrossOriginAuthentication: boolean;
    origin: string;
    constructor(token: string, allowCrossOriginAuthentication?: boolean) {
        this.allowCrossOriginAuthentication = allowCrossOriginAuthentication;
        if (!this.origin) {
            this.origin = options.host;
        }
        // If this is a redirection, don't set the Authorization header
        if (this.origin === options.host || this.allowCrossOriginAuthentication) {
            options.headers['Authorization'] = `Basic ${Buffer.from(`PAT:${this.token}`).toString('base64')}`;
        }
    it('[Basic Auth] - does redirection request with basic auth', async() => {
        const url: string = 'http://microsoft.com';
        const redirectionUrl: string = 'http://jfrog.com';
        const user: string = _authHandlersOptions.basicAuth.username;
        const pass: string = _authHandlersOptions.basicAuth.password;

        //Set nock for redirection with credentials
        const redirectAuthScope = nock(url)
            .get('/')
            .basicAuth({ user, pass })
            .reply(httpm.HttpCodes.MovedPermanently, undefined, {
                location: redirectionUrl
            });

        //Set nock for request without expecting/matching Authorization header(s)
        nock(redirectionUrl)
            .matchHeader('authorization', (val: string | undefined) => !val )
            .get('/')
            .reply(httpm.HttpCodes.OK, {
                success: true,
                source: "nock"
            });

        //Set nock for request with expecting/matching Authorization header(s)
        nock(redirectionUrl)
            .matchHeader('authorization', (val: string | undefined) => val )
            .get('/')
            .reply(httpm.HttpCodes.BadRequest, {
                success: false,
                source: "nock"
            });

        const basicAuthHandler: hm.BasicCredentialHandler = new hm.BasicCredentialHandler(user, pass);
        let httpClient: httpm.HttpClient = new httpm.HttpClient('typed-rest-client-tests', [basicAuthHandler]);
        let httpResponse: httpm.HttpClientResponse = await httpClient.get(url);
        let body: string = await httpResponse.readBody();
        let asJson: any = JSON.parse(body);

        assert(redirectAuthScope.isDone());
        assert(httpResponse.message.statusCode == httpm.HttpCodes.OK, "status code should be 200 - OK");
        assert(asJson.source === "nock", "http get request should be intercepted by nock");
        assert(asJson.success, "Authentication should not occur in redirection to other hosts");
    });

    it('[Personal Access Token] - does redirection request with PAT token auth', async() => {
        const url: string = 'http://microsoft.com';
        const redirectionUrl: string = 'http://jfrog.com';
        const secret: string = _authHandlersOptions.personalAccessToken.secret;
        const personalAccessToken: string = Buffer.from(`PAT:${secret}`).toString('base64');
        const expectedAuthHeader: string = `Basic ${personalAccessToken}`;
        const patAuthHandler: hm.PersonalAccessTokenCredentialHandler =
        new hm.PersonalAccessTokenCredentialHandler(secret);

        //Nock request for redirection with expecting/matching Authorization header(s)
        const redirectAuthScope = nock(url)
            .matchHeader('Authorization', expectedAuthHeader)
            .matchHeader('X-TFS-FedAuthRedirect', 'Suppress')
            .get('/')
            .reply(httpm.HttpCodes.MovedPermanently, undefined, {
                location: redirectionUrl
            });

        //Set nock for request without expecting/matching Authorization header(s)
        nock(redirectionUrl)
            .matchHeader('authorization', (val: string | undefined) => !val )
            .get('/')
            .reply(httpm.HttpCodes.OK, {
                success: true,
                source: "nock"
            });

        //Set nock for request with expecting/matching Authorization header(s)
        nock(redirectionUrl)
            .matchHeader('authorization', (val: string | undefined) => val )
            .get('/')
            .reply(httpm.HttpCodes.BadRequest, {
                success: false,
                source: "nock"
            });

        let httpClient: httpm.HttpClient = new httpm.HttpClient('typed-rest-client-tests', [patAuthHandler]);
        let httpResponse: httpm.HttpClientResponse = await httpClient.get(url);
        let body: string = await httpResponse.readBody();
        let asJson: any = JSON.parse(body);

        assert(redirectAuthScope.isDone());
        assert(httpResponse.message.statusCode == httpm.HttpCodes.OK, "status code should be 200 - OK");
        assert(asJson.source === "nock", "http get request should be intercepted by nock");
        assert(asJson.success, "Authentication should not occur in redirection to other hosts");
    });

    it('[Bearer Token] - does redirection request with bearer token authentication', async() => {
        const url: string = 'http://microsoft.com';
        const redirectionUrl: string = 'http://jfrog.com';
        const bearerToken: string = _authHandlersOptions.bearer.token;

        const expectedAuthHeader: string = `Bearer ${bearerToken}`;
        const bearerTokenAuthHandler: hm.BearerCredentialHandler = new hm.BearerCredentialHandler(bearerToken);

        //Nock request for redirection with expecting/matching Authorization header(s)
        const redirectAuthScope = nock(url)
            .matchHeader('Authorization', expectedAuthHeader)
            .matchHeader('X-TFS-FedAuthRedirect', 'Suppress')
            .get('/')
            .reply(httpm.HttpCodes.MovedPermanently, undefined, {
                location: redirectionUrl
            });

        //Set nock for request without expecting/matching Authorization header(s)
        nock(redirectionUrl)
            .matchHeader('authorization', (val: string | undefined) => !val )
            .get('/')
            .reply(httpm.HttpCodes.OK, {
                success: true,
                source: "nock"
            });

        //Set nock for request with expecting/matching Authorization header(s)
        nock(redirectionUrl)
            .matchHeader('authorization', (val: string | undefined) => val )
            .get('/')
            .reply(httpm.HttpCodes.BadRequest, {
                success: false,
                source: "nock"
            });

        let httpClient: httpm.HttpClient = new httpm.HttpClient('typed-rest-client-tests', [bearerTokenAuthHandler]);
        let httpResponse: httpm.HttpClientResponse = await httpClient.get(url);
        let body: string = await httpResponse.readBody();
        let asJson: any = JSON.parse(body);

        assert(redirectAuthScope.isDone());
        assert(httpResponse.message.statusCode == httpm.HttpCodes.OK, "status code should be 200 - OK");
        assert(asJson.source === "nock", "http get request should be intercepted by nock");
        assert(asJson.success, "Authentication should not occur in redirection to other hosts");
    });