eslint-plugin-zk/src/rules/noLocationHrefAssign.ts

Summary

Maintainability
B
5 hrs
Test Coverage
/* noLocationHrefAssign.ts

    Purpose:
        
    Description:
        
    History:
        11:42 AM 2023/12/25, Created by jumperchen

Copyright (C) 2023 Potix Corporation. All Rights Reserved.
*/
/**
 * @fileoverview prevents xss by assignment to location href javascript url string
 * @author Alexander Mostovenko
 */
import { createRule } from '../util';
import { TSESTree} from '@typescript-eslint/utils';

// ------------------------------------------------------------------------------
// Plugin Definition
// ------------------------------------------------------------------------------

const ERROR = 'Dangerous location.href assignment can lead to XSS';

export const noLocationHrefAssign = createRule({
    name: 'noLocationHrefAssign',
    meta: {
        type: 'problem',
        docs: {
            description: 'disallow location.href assignment (prevent possible XSS)',
            recommended: 'error',
        },
        fixable: 'code',
        messages: {
        },
        schema: [
            {
                type: 'object',
                properties: {
                    escapeFunc: { type: 'string' }
                },
                additionalProperties: false
            }]
    },
    defaultOptions: [],
    create(context) {
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
        const escapeFunc: string = context.options[0] && context.options[0]['escapeFunc'] || 'escape';

        return {
            AssignmentExpression: function (node) {
                const left = node.left as TSESTree.Expression & {property?: {name: string}, object: {name: string, property?: TSESTree.Identifier}};
                // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
                const isHref: boolean = left.property?.name === 'href';
                if (!isHref) {
                    return;
                }
                const isLocationObject: boolean = left.object?.name === 'location';
                const isLocationProperty: boolean | undefined = left.object.property &&
                    (left.object.property as TSESTree.Identifier).name === 'location';

                if (!(isLocationObject || isLocationProperty)) {
                    return;
                }

                const sourceCode = context.getSourceCode();
                if (node.right.type === 'CallExpression' && (node.right.callee.type === 'Identifier') &&
                    (node.right.callee.name === escapeFunc || sourceCode.getText(node.right.callee) === escapeFunc)) {
                    return;
                }
                if (node.right.type === 'CallExpression' && (node.right.callee.type === 'MemberExpression') &&
                    sourceCode.getText(node.right.callee) === escapeFunc) {
                    return;
                }
                // ignore for new URL();
                if (node.right.type === 'MemberExpression' && (node.right.object.type === 'NewExpression') && node.right.object.callee.type === 'Identifier' && (node.right.object.callee.name === 'URL')) {
                    return;
                }

                const rightSource: string = sourceCode.getText(node.right);
                const errorMsg: string = ERROR +
                    '. Please use ' + escapeFunc +
                    '(' + rightSource + ') as a wrapper for escaping';

                // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                // @ts-ignore
                context.report({ node, message: errorMsg });
            }
        };
    }
});