UNPKG

eslint-plugin-sonarjs

Version:
212 lines (211 loc) 10.7 kB
"use strict"; /* * SonarQube JavaScript Plugin * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * You can redistribute and/or modify this program under the terms of * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. * * 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 node_url_1 = require("node:url"); const module_js_1 = require("../helpers/module.js"); const ancestor_js_1 = require("../helpers/ancestor.js"); const ast_js_1 = require("../helpers/ast.js"); const cdk_js_1 = require("../helpers/aws/cdk.js"); // Single source of truth: cleartext scheme → recommended secure alternative. // Mirrors CleartextProtocolFilter.CLEARTEXT_PROTOCOL_ALTERNATIVES from sonar-analyzer-commons. // To add a new protocol, add one entry here — INSECURE_PROTOCOLS and LENIENT_AUTHORITY are derived. const CLEARTEXT_PROTOCOL_ALTERNATIVES = { http: 'https', ftp: 'sftp, scp or ftps', ws: 'wss', telnet: 'ssh', gopher: 'https', tftp: 'sftp', smtp: 'smtps', ldap: 'ldaps', imap: 'imaps', pop3: 'pop3s', amqp: 'amqps', mqtt: 'mqtts', sip: 'sips', rtmp: 'rtmps', irc: 'ircs', nntp: 'nntps', stomp: 'stomps', }; const INSECURE_PROTOCOLS = Object.keys(CLEARTEXT_PROTOCOL_ALTERNATIVES).map(s => `${s}://`); // Internal / non-public hosts — port of CleartextProtocolFilter.SAFE_HOSTS from sonar-analyzer-commons // Note: ^127(?:\.\d+){1,3} covers abbreviated loopback forms (127.1, 127.0.1) that are valid on POSIX systems const SAFE_HOSTS = /(?:^localhost|^127(?:\.\d+){1,3}|^\[(?:0*:){7}:?0*1]|^\[::1]|^169\.254\.\d+\.\d+|^\[fd00:ec2::254]|^168\.63\.129\.16|^100\.100\.100\.200|^metadata\.google\.internal|^metadata\.internal|^host\.docker\.internal|^gateway\.docker\.internal|\.svc\.cluster\.local)(?=:|$)/i; // XML namespace URI authorities — port of CleartextProtocolFilter.NAMESPACE_URI_AUTHORITIES, extended with pre-existing sonar-js exceptions const NAMESPACE_URI_AUTHORITIES = /(?:^www\.w3\.org|^schemas\.android\.com|^schemas\.microsoft\.com|^schemas\.xmlsoap\.org|^www\.sap\.com|^www\.opengis\.net|^hl7\.org|^unitsofmeasure\.org|^purl\.org|^docs\.oasis-open\.org|^xmlns\.com|^json-ld\.org|^schema\.org|^www\.springframework\.org|^maven\.apache\.org|^dublincore\.org|^ogp\.me|^xml\.apache\.org|^schemas\.openxmlformats\.org|^rdfs\.org|^schemas\.google\.com|^a9\.com|^ns\.adobe\.com|^ltsc\.ieee\.org|^docbook\.org|^graphml\.graphdrawing\.org|^json-schema\.org)(?=:|$)/i; // IANA-reserved documentation / placeholder domains — port of CleartextProtocolFilter.DOCUMENTATION_HOSTS const DOCUMENTATION_HOSTS = /(?:(?:^|\.)example\.(?:com|net|org)|\.(?:example|test|localhost))(?=:|$)/i; // Lenient authority extractor for URLs that fail strict URL parsing (e.g. template placeholders, underscores in hostnames). // Derived from CLEARTEXT_PROTOCOL_ALTERNATIVES — no manual update needed when adding protocols. const LENIENT_AUTHORITY = new RegExp(String.raw `^(?:${Object.keys(CLEARTEXT_PROTOCOL_ALTERNATIVES).join('|')}):\/\/(?:[^@\s/?#]+@)?([^\s/?#]+)`, 'i'); 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, ast_js_1.getValueOfExpression)(context, firstArg, 'ObjectExpression'); const ses = (0, ast_js_1.getProperty)(firstArgValue, 'SES', context); if (ses && usesSesCommunication(ses)) { return; } const secure = (0, ast_js_1.getProperty)(firstArgValue, 'secure', context); if (secure && (secure.value.type !== 'Literal' || secure.value.raw !== 'false')) { return; } const requireTls = (0, ast_js_1.getProperty)(firstArgValue, 'requireTLS', context); if (requireTls && (requireTls.value.type !== 'Literal' || requireTls.value.raw !== 'false')) { return; } const port = (0, ast_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, ast_js_1.getValueOfExpression)(context, sesProperty.value, 'ObjectExpression'); if (!configuration) { return false; } const ses = (0, ast_js_1.getValueOfExpression)(context, (0, ast_js_1.getProperty)(configuration, 'ses', context)?.value, 'NewExpression'); if (!ses || (0, cdk_js_1.normalizeFQN)((0, module_js_1.getFullyQualifiedName)(context, ses)) !== '@aws_sdk.client_ses.SES') { return false; } const aws = (0, ast_js_1.getProperty)(configuration, 'aws', context); if (!aws || (0, cdk_js_1.normalizeFQN)((0, module_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, ast_js_1.getValueOfExpression)(context, callExpression.callee.object, 'NewExpression'); if (!!newExpression && (0, module_js_1.getFullyQualifiedName)(context, newExpression.callee) === 'ftp') { const firstArg = callExpression.arguments.length > 0 ? callExpression.arguments[0] : null; if (!firstArg) { return; } const firstArgValue = (0, ast_js_1.getValueOfExpression)(context, firstArg, 'ObjectExpression'); const secure = (0, ast_js_1.getProperty)(firstArgValue, 'secure', context); if (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?.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, ancestor_js_1.getParent)(context, node); return !(parent?.type === 'BinaryExpression' && parent.operator === '+'); } return hasExceptionHost(value); } 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, module_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) { return { messageId: 'insecureProtocol', data: { protocol, alternative: CLEARTEXT_PROTOCOL_ALTERNATIVES[protocol] }, }; } function hasExceptionHost(value) { let host; try { const url = new node_url_1.URL(value); host = url.hostname; if (host.length === 0) { return false; } } catch { // Lenient fallback for template placeholders (e.g. http://localhost:${port}) and underscores in hostnames. // Mirrors CleartextProtocolFilter's authority-extraction logic. const match = LENIENT_AUTHORITY.exec(value); if (!match) { return false; } host = match[1]; } return isSafeHost(host); } function isSafeHost(host) { return (SAFE_HOSTS.test(host) || NAMESPACE_URI_AUTHORITIES.test(host) || DOCUMENTATION_HOSTS.test(host)); }