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

org.sonar.python.checks.hotspots.HashingDataCheck 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.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.CheckForNull;
import org.sonar.check.Rule;
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.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.ExpressionList;
import org.sonar.plugins.python.api.tree.HasSymbol;
import org.sonar.plugins.python.api.tree.ListLiteral;
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.StringLiteral;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.python.checks.AbstractCallExpressionCheck;
import org.sonar.python.checks.utils.Expressions;
import org.sonar.python.tree.TreeUtils;

import static org.sonar.plugins.python.api.tree.Tree.Kind.LIST_LITERAL;
import static org.sonar.plugins.python.api.tree.Tree.Kind.NAME;
import static org.sonar.plugins.python.api.tree.Tree.Kind.STRING_LITERAL;
import static org.sonar.python.checks.utils.Expressions.singleAssignedValue;

@Rule(key = HashingDataCheck.CHECK_KEY)
public class HashingDataCheck extends AbstractCallExpressionCheck {
  public static final String CHECK_KEY = "S4790";
  private static final String MESSAGE = "Make sure that hashing data is safe here.";
  private static final Set questionableFunctions = immutableSet(
    "hashlib.new",
    "cryptography.hazmat.primitives.hashes.SHA1",
    "cryptography.hazmat.primitives.hashes.MD5",
    "django.contrib.auth.hashers.make_password",
    "werkzeug.security.generate_password_hash",
    // https://github.com/Legrandin/pycryptodome
    "Cryptodome.Hash.MD2.new",
    "Cryptodome.Hash.MD4.new",
    "Cryptodome.Hash.MD5.new",
    "Cryptodome.Hash.SHA1.new",
    "Cryptodome.Hash.SHA224.new",
    // https://github.com/dlitz/pycrypto
    "Crypto.Hash.MD2.new",
    "Crypto.Hash.MD4.new",
    "Crypto.Hash.MD5.new",
    "Crypto.Hash.SHA1.new",
    "Crypto.Hash.SHA224.new"
  );
  private static final Set questionableHashlibAlgorithm = immutableSet(
    "hashlib.md5",
    "hashlib.sha1",
    "hashlib.sha224"
  );

  private static final Set unsafeAlgorithms = immutableSet("sha1", "md5", "sha224");

  private static final Set questionablePasslibAlgorithm = Stream.of(
      "apr_md5_crypt", "bigcrypt", "bsd_nthash", "bsdi_crypt",
      "cisco_asa", "cisco_pix", "cisco_type7", "crypt16",
      "des_crypt", "django_des_crypt", "django_salted_md5",
      "django_salted_sha1", "dlitz_pbkdf2_sha1",
      "hex_md4", "hex_md5", "hex_sha1", "ldap_bsdi_crypt", "ldap_des_crypt", "ldap_hex_md5",
      "ldap_plaintext", "ldap_salted_md5",
      "ldap_salted_sha1", "ldap_sha1", "ldap_sha1_crypt", "lmhash", "md5_crypt", "mssql2000",
      "mssql2005", "mysql323", "mysql41", "nthash", "oracle10", "plaintext",
      "postgres_md5", "roundup_plaintext", "sha1_crypt", "sun_md5_crypt")
    .map(hasher -> "passlib.hash." + hasher)
    .collect(Collectors.toSet());


  private static final Set questionableDjangoHashers = Stream.of(
      "SHA1PasswordHasher", "MD5PasswordHasher", "UnsaltedSHA1PasswordHasher",
      "UnsaltedMD5PasswordHasher", "CryptPasswordHasher")
    .map(hasher -> "django.contrib.auth.hashers." + hasher)
    .collect(Collectors.toSet());

  private static final Set questionableHashers = immutableSet("crypt", "unsalted_sha1", "unsalted_md5", "sha1", "md5");

  @Override
  public void initialize(Context context) {
    super.initialize(context);
    context.registerSyntaxNodeConsumer(Tree.Kind.ASSIGNMENT_STMT, HashingDataCheck::checkOverwriteDjangoHashers);
    context.registerSyntaxNodeConsumer(Tree.Kind.CLASSDEF, HashingDataCheck::checkCreatingCustomHasher);
    context.registerSyntaxNodeConsumer(NAME, HashingDataCheck::checkQuestionableHashingAlgorithm);
  }

  @Override
  protected boolean isException(CallExpression callExpression) {
    return isNotUsingUnsafeAlgorithms(callExpression, "django.contrib.auth.hashers.make_password", 2, "hasher", questionableHashers) ||
      isNotUsingUnsafeAlgorithms(callExpression, "hashlib.new", 0, "name", unsafeAlgorithms) ||
      isNotUsingUnsafeAlgorithms(callExpression, "werkzeug.security.generate_password_hash", 1, "method", unsafeAlgorithms) ||
      isHashlibUsedForSecurityArgSetToFalse(callExpression);
  }

  private static boolean isNotUsingUnsafeAlgorithms(CallExpression callExpression, String fqn, int argPosition, String argKeyword,
    Set unsafeAlgorithms) {
    Symbol calleeSymbol = callExpression.calleeSymbol();
    if (calleeSymbol != null && fqn.equals(calleeSymbol.fullyQualifiedName())) {
      RegularArgument argument = TreeUtils.nthArgumentOrKeyword(argPosition, argKeyword, callExpression.arguments());
      if (argument == null) {
        return true;
      }
      StringLiteral value = (StringLiteral) getValue(argument.expression(), STRING_LITERAL);
      return value == null || !unsafeAlgorithms.contains(value.trimmedQuotesValue());
    }
    return false;
  }

  private static boolean isHashlibUsedForSecurityArgSetToFalse(CallExpression callExpression) {
    Symbol calleeSymbol = callExpression.calleeSymbol();
    if (calleeSymbol == null) {
      return false;
    }
    String fqn = calleeSymbol.fullyQualifiedName();
    if (fqn != null && !fqn.startsWith("hashlib.")) {
      return false;
    }

    RegularArgument usedForSecurityArg = TreeUtils.argumentByKeyword("usedforsecurity", callExpression.arguments());
    return usedForSecurityArg != null
      && Expressions.getAssignedName(usedForSecurityArg.expression()).filter(name -> "False".equals(name.name())).isPresent();
  }

  private static void checkOverwriteDjangoHashers(SubscriptionContext ctx) {
    AssignmentStatement assignmentStatementTree = (AssignmentStatement) ctx.syntaxNode();

    if (isOverwritingDjangoHashers(assignmentStatementTree.lhsExpressions(), ctx.pythonFile().fileName())) {
      List weakHashers = getWeakHashers(assignmentStatementTree.assignedValue());
      if (!weakHashers.isEmpty()) {
        PreciseIssue preciseIssue = ctx.addIssue(assignmentStatementTree, MESSAGE);
        weakHashers.forEach(expression -> preciseIssue.secondary(expression, null));
      }
    }
  }

  private static List getWeakHashers(Expression assignedValue) {
    ListLiteral listLiteral = (ListLiteral) getValue(assignedValue, LIST_LITERAL);
    if (listLiteral != null) {
      return listLiteral.elements().expressions().stream()
        .filter(HashingDataCheck::isWeakHasher)
        .toList();
    }
    return Collections.emptyList();
  }

  private static boolean isWeakHasher(Expression expression) {
    StringLiteral value = (StringLiteral) getValue(expression, STRING_LITERAL);
    return value != null && questionableDjangoHashers.contains(value.trimmedQuotesValue());
  }

  @CheckForNull
  private static Expression getValue(Expression expression, Tree.Kind kind) {
    Expression expr = expression;
    if (expression.is(NAME)) {
      expr = singleAssignedValue(((Name) expression));
    }
    if (expr != null && expr.is(kind)) {
      return expr;
    }
    return null;
  }

  private static boolean isOverwritingDjangoHashers(List lhsExpressions, String filename) {
    // checks for `PASSWORD_HASHERS = []` in a global_settings.py file
    if (filename.equals("global_settings.py") &&
      lhsExpressions.stream()
        .flatMap(pelt -> pelt.expressions().stream())
        .anyMatch(expression -> expression.firstToken().value().equals("PASSWORD_HASHERS"))) {

      return true;
    }
    // checks for `settings.PASSWORD_HASHERS = value`
    for (ExpressionList expr : lhsExpressions) {
      for (Expression expression : expr.expressions()) {
        Expression baseExpr = Expressions.removeParentheses(expression);
        if (baseExpr.is(Tree.Kind.QUALIFIED_EXPR)) {
          QualifiedExpression qualifiedExpression = (QualifiedExpression) baseExpr;
          Symbol symbol = qualifiedExpression.symbol();
          if (symbol != null && "django.conf.settings.PASSWORD_HASHERS".equals(symbol.fullyQualifiedName())) {
            return true;
          }
        }
      }
    }
    return false;
  }

  private static void checkQuestionableHashingAlgorithm(SubscriptionContext ctx) {
    Name name = (Name) ctx.syntaxNode();
    if (isWithinImport(name)) {
      return;
    }

    Tree callTree = TreeUtils.firstAncestorOfKind(name, Tree.Kind.CALL_EXPR);
    if (callTree instanceof CallExpression callExpr && isHashlibUsedForSecurityArgSetToFalse(callExpr)) {
      return;
    }

    String fullyQualifiedName = name.symbol() != null ? name.symbol().fullyQualifiedName() : "";
    if (questionableHashlibAlgorithm.contains(fullyQualifiedName) || questionablePasslibAlgorithm.contains(fullyQualifiedName)) {
      ctx.addIssue(name, MESSAGE);
    }
  }

  private static String getQualifiedName(Expression node) {
    if (node instanceof HasSymbol hasSymbol) {
      Symbol symbol = hasSymbol.symbol();
      return symbol != null ? symbol.fullyQualifiedName() : "";
    }
    return "";
  }

  private static void checkCreatingCustomHasher(SubscriptionContext ctx) {
    ClassDef classDef = (ClassDef) ctx.syntaxNode();
    ArgList argList = classDef.args();
    if (argList != null) {
      argList.arguments()
        .stream()
        .filter(arg -> arg.is(Tree.Kind.REGULAR_ARGUMENT))
        .map(RegularArgument.class::cast)
        .filter(arg -> questionableDjangoHashers.contains(getQualifiedName(arg.expression())))
        .forEach(arg -> ctx.addIssue(arg, MESSAGE));
    }
  }

  @Override
  protected Set functionsToCheck() {
    return questionableFunctions;
  }

  @Override
  protected String message() {
    return MESSAGE;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy