eslint-plugin-sonarjs
Version:
SonarJS rules for ESLint
203 lines (202 loc) • 8.74 kB
JavaScript
/*
* 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/S5332/javascript
Object.defineProperty(exports, "__esModule", { value: true });
exports.rule = void 0;
const url_1 = require("url");
const index_js_1 = require("../helpers/index.js");
const cdk_js_1 = require("../helpers/aws/cdk.js");
const INSECURE_PROTOCOLS = ['http://', 'ftp://', 'telnet://'];
const LOOPBACK_PATTERN = /localhost|127(?:\.\d+){0,2}\.\d+$|\/\/(?:0*:)*?:?0*1$/;
const EXCEPTION_FULL_HOSTS = [
'www.w3.org',
'xml.apache.org',
'schemas.xmlsoap.org',
'schemas.openxmlformats.org',
'rdfs.org',
'purl.org',
'xmlns.com',
'schemas.google.com',
'a9.com',
'ns.adobe.com',
'ltsc.ieee.org',
'docbook.org',
'graphml.graphdrawing.org',
'json-schema.org',
'schemas.microsoft.com',
];
const EXCEPTION_TOP_HOSTS = [
/\.example$/,
/\.?example\.com$/,
/\.?example\.org$/,
/\.test$/,
/\.?test\.com$/,
];
exports.rule = {
meta: {
messages: {
insecureProtocol: 'Using {{protocol}} protocol is insecure. Use {{alternative}} instead.',
},
},
create(context) {
function checkNodemailer(callExpression) {
const firstArg = callExpression.arguments.length > 0 ? callExpression.arguments[0] : null;
if (!firstArg) {
return;
}
const firstArgValue = (0, index_js_1.getValueOfExpression)(context, firstArg, 'ObjectExpression');
const ses = (0, index_js_1.getProperty)(firstArgValue, 'SES', context);
if (ses && usesSesCommunication(ses)) {
return;
}
const secure = (0, index_js_1.getProperty)(firstArgValue, 'secure', context);
if (secure && (secure.value.type !== 'Literal' || secure.value.raw !== 'false')) {
return;
}
const requireTls = (0, index_js_1.getProperty)(firstArgValue, 'requireTLS', context);
if (requireTls && (requireTls.value.type !== 'Literal' || requireTls.value.raw !== 'false')) {
return;
}
const port = (0, index_js_1.getProperty)(firstArgValue, 'port', context);
if (port && (port.value.type !== 'Literal' || port.value.raw === '465')) {
return;
}
context.report({ node: callExpression.callee, ...getMessageAndData('http') });
}
function usesSesCommunication(sesProperty) {
const configuration = (0, index_js_1.getValueOfExpression)(context, sesProperty.value, 'ObjectExpression');
if (!configuration) {
return false;
}
const ses = (0, index_js_1.getValueOfExpression)(context, (0, index_js_1.getProperty)(configuration, 'ses', context)?.value, 'NewExpression');
if (!ses || (0, cdk_js_1.normalizeFQN)((0, index_js_1.getFullyQualifiedName)(context, ses)) !== '@aws_sdk.client_ses.SES') {
return false;
}
const aws = (0, index_js_1.getProperty)(configuration, 'aws', context);
if (!aws ||
(0, cdk_js_1.normalizeFQN)((0, index_js_1.getFullyQualifiedName)(context, aws.value)) !== '@aws_sdk.client_ses') {
return false;
}
return true;
}
function checkCallToFtp(callExpression) {
if (callExpression.callee.type === 'MemberExpression' &&
callExpression.callee.property.type === 'Identifier' &&
callExpression.callee.property.name === 'connect') {
const newExpression = (0, index_js_1.getValueOfExpression)(context, callExpression.callee.object, 'NewExpression');
if (!!newExpression && (0, index_js_1.getFullyQualifiedName)(context, newExpression.callee) === 'ftp') {
const firstArg = callExpression.arguments.length > 0 ? callExpression.arguments[0] : null;
if (!firstArg) {
return;
}
const firstArgValue = (0, index_js_1.getValueOfExpression)(context, firstArg, 'ObjectExpression');
const secure = (0, index_js_1.getProperty)(firstArgValue, 'secure', context);
if (secure && secure.value.type === 'Literal' && secure.value.raw === 'false') {
context.report({
node: callExpression.callee,
...getMessageAndData('ftp'),
});
}
}
}
}
function checkCallToRequire(callExpression) {
if (callExpression.callee.type === 'Identifier' && callExpression.callee.name === 'require') {
const firstArg = callExpression.arguments.length > 0 ? callExpression.arguments[0] : null;
if (firstArg &&
firstArg.type === 'Literal' &&
typeof firstArg.value === 'string' &&
firstArg.value === 'telnet-client') {
context.report({
node: firstArg,
...getMessageAndData('telnet'),
});
}
}
}
function isExceptionUrl(value, node) {
if (INSECURE_PROTOCOLS.includes(value)) {
const parent = (0, index_js_1.getParent)(context, node);
return !(parent?.type === 'BinaryExpression' && parent.operator === '+');
}
return hasExceptionHost(value);
}
function hasExceptionHost(value) {
let url;
try {
url = new url_1.URL(value);
}
catch (err) {
return false;
}
const host = url.hostname;
return (host.length === 0 ||
LOOPBACK_PATTERN.test(host) ||
EXCEPTION_FULL_HOSTS.some(exception => exception === host) ||
EXCEPTION_TOP_HOSTS.some(exception => exception.test(host)));
}
return {
Literal: (node) => {
const literal = node;
if (typeof literal.value === 'string') {
const value = literal.value.trim().toLocaleLowerCase();
const insecure = INSECURE_PROTOCOLS.find(protocol => value.startsWith(protocol));
if (insecure && !isExceptionUrl(value, node)) {
const protocol = insecure.substring(0, insecure.indexOf(':'));
context.report({
...getMessageAndData(protocol),
node,
});
}
}
},
CallExpression: (node) => {
const callExpression = node;
if ((0, index_js_1.getFullyQualifiedName)(context, callExpression) === 'nodemailer.createTransport') {
checkNodemailer(callExpression);
}
checkCallToFtp(callExpression);
checkCallToRequire(callExpression);
},
ImportDeclaration: (node) => {
const importDeclaration = node;
if (typeof importDeclaration.source.value === 'string' &&
importDeclaration.source.value === 'telnet-client') {
context.report({
node: importDeclaration.source,
...getMessageAndData('telnet'),
});
}
},
};
},
};
function getMessageAndData(protocol) {
let alternative;
switch (protocol) {
case 'http':
alternative = 'https';
break;
case 'ftp':
alternative = 'sftp, scp or ftps';
break;
default:
alternative = 'ssh';
}
return { messageId: 'insecureProtocol', data: { protocol, alternative } };
}
;