CVE-2023-30846 (TypeScript, published 2023-04-26T21:15Z)
Description: 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.
Severity: nan (CVSS v3 base score: 7.5)
Weakness: CWE-522 \u2014 Insufficiently Protected Credentials
The product transmits or stores authentication credentials, but it uses an insecure method that is susceptible to unauthorized interception and/or retrieval.
Repository: https://github.com/microsoft/typed-rest-client
Files changed:
lib/handlers/basiccreds.ts
lib/handlers/bearertoken.ts
lib/handlers/personalaccesstoken.ts
test/units/handlers.ts
Commit message: Disable authentications on redirections (#207)

* Disable authentications on redirections

* Add allowCrossOriginAuthentication boolean

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");
    });