CVE-2023-30846 (published 2023-04-26T21:15Z)

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.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-522
cwe_name: Insufficiently Protected Credentials
description: The product transmits or stores authentication credentials, but it uses an insecure method that is susceptible to unauthorized interception and/or retrieval.
extended_description: 
            
            
            
         
         

Fix information:
repo_url: https://github.com/microsoft/typed-rest-client
commit_details: repo_url: https://github.com/microsoft/typed-rest-client
author: Yahav Itzhak
author_date: 2020-04-01 16:08:10+03:00
msg: Disable authentications on redirections (#207)

* Disable authentications on redirections

* Add allowCrossOriginAuthentication boolean
repository: repo_url: https://github.com/microsoft/typed-rest-client
repo_name: microsoft/typed-rest-client
description: Node Rest and Http Clients with typings for use with TypeScript
repo_language: TypeScript
owner: microsoft
file_changes: filename: basiccreds.ts
change_type: ModificationType.MODIFY
diff: @@ -6,16 +6,25 @@ 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';
     }
 

programming_language: TypeScript
filename: bearertoken.ts
change_type: ModificationType.MODIFY
diff: @@ -5,15 +5,24 @@ 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';
     }
 

programming_language: TypeScript
filename: personalaccesstoken.ts
change_type: ModificationType.MODIFY
diff: @@ -5,15 +5,24 @@ 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';
     }
 

programming_language: TypeScript
filename: handlers.ts
change_type: ModificationType.MODIFY
diff: @@ -80,6 +80,50 @@ describe('Authentication Handlers Tests', function () {
         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;
@@ -165,6 +209,53 @@ describe('Authentication Handlers Tests', function () {
         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;
@@ -216,6 +307,52 @@ describe('Authentication Handlers Tests', function () {
         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:

programming_language: TypeScript