CVE-2021-23384 (JavaScript)

The package koa-remove-trailing-slashes before 2.0.2 are vulnerable to Open Redirect via the use of trailing double slashes in the URL when accessing the vulnerable endpoint (such as https://example.com//attacker.example/). The vulnerable code is in index.js::removeTrailingSlashes(), as the web server uses relative URLs instead of absolute URLs.

Weakness:
CWE-601: name: URL Redirection to Untrusted Site ('Open Redirect')
description: The web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a redirect.

Repository: https://github.com/vgno/koa-remove-trailing-slashes

Vulnerable code:
file_path: index.js
snippet: function removeTrailingSlashes(opts) {
    opts = opts || {};

    if (opts.defer !== false) {
        opts.defer = opts.defer || true;
    }

    if (opts.chained !== false) {
        opts.chained = opts.chained || true;
    }

    return async function(ctx, next) {
        if (opts.defer) {
            await next();
        }

        let path;
        let querystring = '';

        // We have already done a redirect and we will continue if we are in chained mode
        if (opts.chained && ctx.status === 301) {
            const location = ctx.response.get('Location') || '';

            // We can't use ctx.querystring because it may not be up to date
            const parsedLocation = location.match(/\?(.*)$/);
            if (parsedLocation && parsedLocation[1]) {
                querystring = parsedLocation[1];
            }

            path = getPath(location, querystring);
        } else if (ctx.status !== 301) {
            querystring = ctx.querystring;
            path = getPath(ctx.originalUrl, ctx.querystring);
        }

        if (path && haveSlash(path)) {
            path = path.slice(0, -1);
            const query = querystring.length ? '?' + querystring : '';

            ctx.status = 301;
            ctx.redirect(path + query);
        }

        if (!opts.defer) {
            await next();
        }
    };
}

Fixed code:
file_path: index.js
snippet: function removeTrailingSlashes(opts) {
    opts = opts || {};

    if (opts.defer !== false) {
        opts.defer = opts.defer || true;
    }

    if (opts.chained !== false) {
        opts.chained = opts.chained || true;
    }

    return async function(ctx, next) {
        if (opts.defer) {
            await next();
        }

        let path;
        let querystring = '';

        // We have already done a redirect and we will continue if we are in chained mode
        if (opts.chained && ctx.status === 301) {
            const location = ctx.response.get('Location') || '';

            // We can't use ctx.querystring because it may not be up to date
            const parsedLocation = location.match(/\?(.*)$/);
            if (parsedLocation && parsedLocation[1]) {
                querystring = parsedLocation[1];
            }

            path = getPath(location, querystring);
        } else if (ctx.status !== 301) {
            querystring = ctx.querystring;
            path = getPath(ctx.originalUrl, ctx.querystring);
        }

        if (path && haveSlash(path)) {
            path = path.slice(0, -1);
            const query = querystring.length ? '?' + querystring : '';

            const newURL = new URL(path + query, ctx.request.URL);

            if (ctx.origin === newURL.origin) {
                ctx.status = 301;
                ctx.redirect(path + query);
            }
        }

        if (!opts.defer) {
            await next();
        }
    };
}