eslint-plugin-sonarjs
Version:
SonarJS rules for ESLint
315 lines (314 loc) • 11.1 kB
JavaScript
"use strict";
/*
* SonarQube JavaScript Plugin
* Copyright (C) 2011-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
// https://sonarsource.github.io/rspec/#/rspec/S2819/javascript
Object.defineProperty(exports, "__esModule", { value: true });
exports.rule = void 0;
const index_js_1 = require("../helpers/index.js");
const meta_js_1 = require("./meta.js");
const POST_MESSAGE = 'postMessage';
const ADD_EVENT_LISTENER = 'addEventListener';
exports.rule = {
meta: (0, index_js_1.generateMeta)(meta_js_1.meta, {
messages: {
specifyTarget: `Specify a target origin for this message.`,
verifyOrigin: `Verify the origin of the received message.`,
},
}),
create(context) {
const services = context.sourceCode.parserServices;
if (!(0, index_js_1.isRequiredParserServices)(services)) {
return {};
}
return {
[`CallExpression:matches([callee.name="${POST_MESSAGE}"], [callee.property.name="${POST_MESSAGE}"])`]: (node) => {
checkPostMessageCall(node, context);
},
[`CallExpression[callee.property.name="${ADD_EVENT_LISTENER}"]`]: (node) => {
checkAddEventListenerCall(node, context);
},
};
},
};
function isWindowObject(node, context) {
const type = (0, index_js_1.getTypeAsString)(node, context.sourceCode.parserServices);
const hasWindowName = WindowNameVisitor.containsWindowName(node, context);
return type.match(/window/i) || type.match(/globalThis/i) || hasWindowName;
}
function checkPostMessageCall(callExpr, context) {
const { callee } = callExpr;
// Window.postMessage() can take 2 or 3 arguments
if (![2, 3].includes(callExpr.arguments.length) ||
(0, index_js_1.getValueOfExpression)(context, callExpr.arguments[1], 'Literal')?.value !== '*') {
return;
}
if (callee.type === 'Identifier') {
context.report({
node: callee,
messageId: 'specifyTarget',
});
}
if (callee.type !== 'MemberExpression') {
return;
}
if (isWindowObject(callee.object, context)) {
context.report({
node: callee,
messageId: 'specifyTarget',
});
}
}
function checkAddEventListenerCall(callExpr, context) {
const { callee, arguments: args } = callExpr;
if (!isWindowObject(callee, context) ||
args.length < 2 ||
!isMessageTypeEvent(args[0], context)) {
return;
}
let listener = (0, index_js_1.resolveFunction)(context, args[1]);
if (listener?.body.type === 'CallExpression') {
listener = (0, index_js_1.resolveFunction)(context, listener.body);
}
if (!listener || listener.params.length === 0) {
return;
}
const event = listener.params[0];
if (event.type !== 'Identifier') {
return;
}
if (!hasVerifiedOrigin(context, listener, event)) {
context.report({
node: callee,
messageId: 'verifyOrigin',
});
}
}
/**
* Checks if event.origin or event.originalEvent.origin is verified
*/
function hasVerifiedOrigin(context, listener, event) {
const scope = context.sourceCode.scopeManager.acquire(listener);
const eventVariable = scope?.variables.find(v => v.name === event.name);
if (eventVariable) {
const eventIdentifiers = eventVariable.references.map(e => e.identifier);
for (const reference of eventVariable.references) {
const eventRef = reference.identifier;
if (isEventOriginCompared(eventRef) || isEventOriginalEventCompared(eventRef)) {
return true;
}
// event OR-ed with event.originalEvent
const unionEvent = findUnionEvent(eventRef, eventIdentifiers);
if (unionEvent !== null && isReferenceCompared(scope, unionEvent)) {
return true;
}
// event.origin OR-ed with event.originalEvent.origin
const unionOrigin = findUnionOrigin(eventRef, eventIdentifiers);
if (unionOrigin !== null &&
isEventOriginReferenceCompared(scope, unionOrigin)) {
return true;
}
}
}
return false;
/**
* Looks for unionEvent = event | event.originalEvent
*/
function findUnionEvent(event, eventIdentifiers) {
const logicalExpr = event.parent;
if (logicalExpr?.type !== 'LogicalExpression') {
return null;
}
if ((logicalExpr.left === event &&
isEventOriginalEvent(logicalExpr.right, eventIdentifiers)) ||
(logicalExpr.right === event &&
isEventOriginalEvent(logicalExpr.left, eventIdentifiers))) {
return extractVariableDeclaratorIfExists(logicalExpr);
}
return null;
}
/**
* looks for unionOrigin = event.origin | event.originalEvent.origin
*/
function findUnionOrigin(eventRef, eventIdentifiers) {
const memberExpr = eventRef.parent;
// looks for event.origin in a LogicalExpr
if (!(memberExpr?.type === 'MemberExpression' && memberExpr.parent?.type === 'LogicalExpression')) {
return null;
}
const logicalExpr = memberExpr.parent;
if (!(logicalExpr.left === memberExpr &&
isEventOriginalEventOrigin(logicalExpr.right, eventIdentifiers)) &&
!(logicalExpr.right === memberExpr &&
isEventOriginalEventOrigin(logicalExpr.left, eventIdentifiers))) {
return null;
}
return extractVariableDeclaratorIfExists(logicalExpr);
/**
* checks if memberExpr is event.originalEvent.origin
*/
function isEventOriginalEventOrigin(memberExpr, eventIdentifiers) {
const subMemberExpr = memberExpr.object;
if (subMemberExpr?.type !== 'MemberExpression') {
return false;
}
const isOrigin = memberExpr.property.type === 'Identifier' && memberExpr.property.name === 'origin';
return isEventOriginalEvent(subMemberExpr, eventIdentifiers) && isOrigin;
}
}
}
/**
* Looks for an occurence of the provided node in an IfStatement
*/
function isReferenceCompared(scope, identifier) {
function getGrandParent(node) {
return node?.parent?.parent;
}
return checkReference(scope, identifier, getGrandParent);
}
/**
* checks if a reference of identifier is
* node.identifier
*/
function isEventOriginReferenceCompared(scope, identifier) {
function getParent(node) {
return node?.parent;
}
return checkReference(scope, identifier, getParent);
}
/**
*
*/
function checkReference(scope, identifier, callback) {
const identifierVariable = scope?.variables.find(v => v.name === identifier.name);
if (!identifierVariable) {
return false;
}
for (const reference of identifierVariable.references) {
const binaryExpressionCandidate = callback(reference.identifier);
if (isInIfStatement(binaryExpressionCandidate)) {
return true;
}
}
return false;
}
/**
* checks if memberExpr is event.originalEvent
*/
function isEventOriginalEvent(memberExpr, eventIdentifiers) {
const isEvent = memberExpr.object.type === 'Identifier' &&
eventIdentifiers.includes(memberExpr.object);
const isOriginalEvent = memberExpr.property.type === 'Identifier' && memberExpr.property.name === 'originalEvent';
return isEvent && isOriginalEvent;
}
/**
* Extracts the identifier to which the 'node' expression is assigned to
*/
function extractVariableDeclaratorIfExists(node) {
if (node.parent?.type !== 'VariableDeclarator') {
return null;
}
return node.parent.id;
}
/**
* Looks for an IfStatement with event.origin
*/
function isEventOriginCompared(event) {
const memberExpr = findEventOrigin(event);
return isInIfStatement(memberExpr);
}
/**
* Looks for an IfStatement with event.originalEvent.origin
*/
function isEventOriginalEventCompared(event) {
const eventOriginalEvent = findEventOriginalEvent(event);
if (!eventOriginalEvent?.parent) {
return false;
}
if (!isPropertyOrigin(eventOriginalEvent.parent)) {
return false;
}
return isInIfStatement(eventOriginalEvent);
}
/**
* Returns event.origin MemberExpression, if exists
*/
function findEventOrigin(event) {
const parent = event.parent;
if (parent?.type !== 'MemberExpression') {
return null;
}
if (isPropertyOrigin(parent)) {
return parent;
}
else {
return null;
}
}
/**
* Checks if node has a property 'origin'
*/
function isPropertyOrigin(node) {
return (0, index_js_1.isIdentifier)(node.property, 'origin');
}
/**
* Returns event.originalEvent MemberExpression, if exists
*/
function findEventOriginalEvent(event) {
const memberExpr = event.parent;
if (memberExpr?.type !== 'MemberExpression') {
return null;
}
const { object: eventCandidate, property: originalEventIdentifierCandidate } = memberExpr;
if (eventCandidate === event &&
(0, index_js_1.isIdentifier)(originalEventIdentifierCandidate, 'originalEvent')) {
return memberExpr;
}
return null;
}
/**
* Checks if the current node is nested in an IfStatement
*/
function isInIfStatement(node) {
// this checks for 'undefined' and 'null', because node.parent can be 'null'
if (node == null) {
return false;
}
return (0, index_js_1.findFirstMatchingLocalAncestor)(node, index_js_1.isIfStatement) != null;
}
function isMessageTypeEvent(eventNode, context) {
const eventValue = (0, index_js_1.getValueOfExpression)(context, eventNode, 'Literal');
return typeof eventValue?.value === 'string' && eventValue.value.toLowerCase() === 'message';
}
class WindowNameVisitor {
constructor() {
this.hasWindowName = false;
}
static containsWindowName(node, context) {
const visitor = new WindowNameVisitor();
visitor.visit(node, context);
return visitor.hasWindowName;
}
visit(root, context) {
const visitNode = (node) => {
if (node.type === 'Identifier' && node.name.match(/window/i)) {
this.hasWindowName = true;
}
(0, index_js_1.childrenOf)(node, context.sourceCode.visitorKeys).forEach(visitNode);
};
visitNode(root);
}
}