All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonar.python.checks.hotspots.ClearTextProtocolsCheck Maven / Gradle / Ivy

There is a newer version: 4.23.0.17664
Show newest version
/*
 * SonarQube Python Plugin
 * Copyright (C) 2011-2024 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/
 */
package org.sonar.python.checks.hotspots;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.sonar.check.Rule;
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
import org.sonar.plugins.python.api.SubscriptionContext;
import org.sonar.plugins.python.api.symbols.Symbol;
import org.sonar.plugins.python.api.tree.ArgList;
import org.sonar.plugins.python.api.tree.Argument;
import org.sonar.plugins.python.api.tree.AssignmentStatement;
import org.sonar.plugins.python.api.tree.CallExpression;
import org.sonar.plugins.python.api.tree.ClassDef;
import org.sonar.plugins.python.api.tree.Expression;
import org.sonar.plugins.python.api.tree.HasSymbol;
import org.sonar.plugins.python.api.tree.Name;
import org.sonar.plugins.python.api.tree.QualifiedExpression;
import org.sonar.plugins.python.api.tree.RegularArgument;
import org.sonar.plugins.python.api.tree.StringElement;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.python.checks.utils.Expressions;
import org.sonar.python.checks.cdk.ClearTextProtocolsCheckPart;
import org.sonar.python.tree.TreeUtils;

@Rule(key = "S5332")
public class ClearTextProtocolsCheck extends PythonSubscriptionCheck {
  private static final List SENSITIVE_PROTOCOLS = Arrays.asList("http://", "ftp://", "telnet://");
  private static final Pattern LOOPBACK = Pattern.compile("localhost|127(?:\\.[0-9]+){0,2}\\.[0-9]+$|^(?:0*\\:)*?:?0*1", Pattern.CASE_INSENSITIVE);
  private static final Map ALTERNATIVES = Map.of(
    "http", "https",
    "ftp", "sftp, scp or ftps",
    "telnet", "ssh");
  private static final String SENSITIVE_HTTP_SERVER_START_FQN = "socketserver.BaseServer.serve_forever";
  private static final String SENSITIVE_HTTP_SERVER_BIND_FQN = "socketserver.BaseServer.server_bind";
  private static final Set SENSITIVE_HTTP_SERVER_METHOD_NAMES = Set.of("serve_forever", "server_bind");
  private static final Set SENSITIVE_HTTP_SERVER_CLASSES = Set.of("http.server.HTTPServer", "http.server.ThreadingHTTPServer");

  @Override
  public void initialize(Context context) {
    context.registerSyntaxNodeConsumer(Tree.Kind.STRING_ELEMENT, ctx -> {
      Tree node = ctx.syntaxNode();
      String value = Expressions.unescape((StringElement) node);
      unsafeProtocol(value)
        // cleanup slashes
        .map(protocol -> protocol.substring(0, protocol.length() - 3))
        .ifPresent(protocol -> ctx.addIssue(node, message(protocol)));
    });
    context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, ctx -> {
      CallExpression callExpression = (CallExpression) ctx.syntaxNode();
      Optional.ofNullable(callExpression.calleeSymbol())
        .map(Symbol::fullyQualifiedName)
        .flatMap(ClearTextProtocolsCheck::isUnsafeLib)
        .ifPresent(protocol -> ctx.addIssue(callExpression, message(protocol)));
    });

    context.registerSyntaxNodeConsumer(Tree.Kind.ASSIGNMENT_STMT, ctx -> handleAssignmentStatement((AssignmentStatement) ctx.syntaxNode(), ctx));

    context.registerSyntaxNodeConsumer(Tree.Kind.QUALIFIED_EXPR, ClearTextProtocolsCheck::checkServerCallFromSuper);

    context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, ClearTextProtocolsCheck::checkServerBindCalls);

    new ClearTextProtocolsCheckPart().initialize(context);
  }

  private static void checkServerCallFromSuper(SubscriptionContext ctx) {
    QualifiedExpression qualifiedExpression = (QualifiedExpression) ctx.syntaxNode();
    Optional.of(qualifiedExpression)
      .filter(qe -> SENSITIVE_HTTP_SERVER_METHOD_NAMES.contains(qe.name().name()))
      .filter(ClearTextProtocolsCheck::isCallToSensitiveSuperClass)
      .map(qe -> TreeUtils.firstAncestorOfKind(qe, Tree.Kind.CALL_EXPR))
      .flatMap(TreeUtils.toOptionalInstanceOfMapper(CallExpression.class))
      .ifPresent(ce -> ctx.addIssue(ce, message("http")));
  }

  private static void checkServerBindCalls(SubscriptionContext ctx) {
    CallExpression callExpression = (CallExpression) ctx.syntaxNode();
    Optional.ofNullable(callExpression.calleeSymbol())
      .map(Symbol::fullyQualifiedName)
      .filter(SENSITIVE_HTTP_SERVER_BIND_FQN::equals)
      .filter(fqn -> isParentClassExtendingSensitiveClass(callExpression))
      .ifPresent(fqn -> ctx.addIssue(callExpression, message("http")));
  }

  private static boolean isCallToSensitiveSuperClass(QualifiedExpression expression) {
    return Optional.of(expression.qualifier())
      .flatMap(TreeUtils.toOptionalInstanceOfMapper(CallExpression.class))
      .map(CallExpression::callee)
      .flatMap(TreeUtils.toOptionalInstanceOfMapper(Name.class))
      .map(Name::name)
      .filter("super"::equals)
      .filter(name -> isParentClassExtendingSensitiveClass(expression))
      .isPresent();
  }

  private static boolean isParentClassExtendingSensitiveClass(Tree expression) {
    return Optional.ofNullable(TreeUtils.firstAncestorOfKind(expression, Tree.Kind.CLASSDEF))
      .map(ClassDef.class::cast)
      .map(ClassDef::args)
      .map(ArgList::arguments)
      .map(ClearTextProtocolsCheck::getClassFQNFromArgument)
      .map(arguments -> arguments.anyMatch(SENSITIVE_HTTP_SERVER_CLASSES::contains))
      .orElse(false);
  }

  private static Stream getClassFQNFromArgument(List arguments) {
    return arguments.stream()
      .map(TreeUtils.toInstanceOfMapper(RegularArgument.class))
      .filter(Objects::nonNull)
      .map(RegularArgument::expression)
      .filter(HasSymbol.class::isInstance)
      .map(HasSymbol.class::cast)
      .map(HasSymbol::symbol)
      .filter(Objects::nonNull)
      .map(Symbol::fullyQualifiedName);
  }

  private static void handleAssignmentStatement(AssignmentStatement assignmentStatement, SubscriptionContext ctx) {
    if (assignmentStatement.lhsExpressions().size() > 1) {
      // avoid potential FPs
      return;
    }
    Expression lhs = assignmentStatement.lhsExpressions().get(0).expressions().get(0);
    if (lhs instanceof HasSymbol hasSymbol) {
      Symbol symbol = hasSymbol.symbol();
      if (symbol == null) {
        return;
      }
      if (lhs.type().canOnlyBe("smtplib.SMTP")) {
        boolean usesEncryption = symbol.usages().stream().anyMatch(u -> {
          Tree tree = TreeUtils.firstAncestorOfKind(u.tree(), Tree.Kind.CALL_EXPR);
          if (tree != null) {
            Symbol calleeSymbol = ((CallExpression) tree).calleeSymbol();
            return calleeSymbol != null && "smtplib.SMTP.starttls".equals(calleeSymbol.fullyQualifiedName());
          }
          return false;
        });
        if (!usesEncryption) {
          ctx.addIssue(assignmentStatement.assignedValue(), "Make sure STARTTLS is used to upgrade to a secure connection using SSL/TLS.");
        }
      }
    }
  }

  private static Optional unsafeProtocol(String literalValue) {
    for (String protocol : SENSITIVE_PROTOCOLS) {
      if (literalValue.startsWith(protocol)) {
        try {
          URI uri = new URI(literalValue);
          String host = uri.getHost();
          if (host == null) {
            // handle ipv6 loopback
            host = uri.getAuthority();
          }
          if (host == null || LOOPBACK.matcher(host).matches()) {
            return Optional.empty();
          }
        } catch (URISyntaxException e) {
          // not parseable uri, try to find loopback in the substring without protocol, this handles case of url formatted as string
          if (LOOPBACK.matcher(literalValue.substring(protocol.length())).find()) {
            return Optional.empty();
          }
        }
        return Optional.of(protocol);
      }
    }
    return Optional.empty();
  }

  private static Optional isUnsafeLib(String qualifiedName) {
    if ("telnetlib.Telnet".equals(qualifiedName)) {
      return Optional.of("telnet");
    }
    if ("ftplib.FTP".equals(qualifiedName)) {
      return Optional.of("ftp");
    }
    if (SENSITIVE_HTTP_SERVER_START_FQN.equals(qualifiedName)) {
      return Optional.of("http");
    }
    return Optional.empty();
  }

  private static String message(String protocol) {
    return "Using " + protocol + " protocol is insecure. Use " + ALTERNATIVES.get(protocol) + " instead";
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy