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

org.sonar.java.checks.security.CookieHttpOnlyCheck Maven / Gradle / Ivy

There is a newer version: 8.10.0.38194
Show newest version
/*
 * SonarQube Java
 * Copyright (C) 2012-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.java.checks.security;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.check.Rule;
import org.sonar.java.checks.helpers.ExpressionsHelper;
import org.sonar.java.model.LiteralUtils;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.semantic.MethodMatchers;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.Symbol.VariableSymbol;
import org.sonar.plugins.java.api.tree.Arguments;
import org.sonar.plugins.java.api.tree.AssignmentExpressionTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.IdentifierTree;
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
import org.sonar.plugins.java.api.tree.NewClassTree;
import org.sonar.plugins.java.api.tree.ReturnStatementTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.TypeTree;
import org.sonar.plugins.java.api.tree.VariableTree;

@Rule(key = "S3330")
public class CookieHttpOnlyCheck extends IssuableSubscriptionVisitor {
  private final Set ignoredVariables = new HashSet<>();
  private final Map symbolConstructorMapToReport = new LinkedHashMap<>();
  private final List settersToReport = new ArrayList<>();
  private final List newClassToReport = new ArrayList<>();

  private static final List IGNORED_COOKIE_NAMES = Arrays.asList("csrf", "xsrf");

  private static final String JAVA_LANG_STRING = "java.lang.String";
  private static final String JAVA_UTIL_DATE = "java.util.Date";
  private static final String INT = "int";
  private static final String BOOLEAN = "boolean";

  private static final String MESSAGE = "Make sure creating this cookie without the \"HttpOnly\" flag is safe.";

  private static final int COOKIE_NAME_ARGUMENT = 0;

  private static final class ClassName {
    private static final String SERVLET_COOKIE = "javax.servlet.http.Cookie";
    private static final String JAKARTA_SERVLET_COOKIE = "jakarta.servlet.http.Cookie";
    private static final String NET_HTTP_COOKIE = "java.net.HttpCookie";
    private static final String JAX_RS_COOKIE = "javax.ws.rs.core.Cookie";
    private static final String JAKARTA_RS_COOKIE = "jakarta.ws.rs.core.Cookie";
    private static final String JAX_RS_NEW_COOKIE = "javax.ws.rs.core.NewCookie";
    private static final String JAKARTA_RS_NEW_COOKIE = "jakarta.ws.rs.core.NewCookie";
    private static final String SHIRO_COOKIE = "org.apache.shiro.web.servlet.SimpleCookie";
    private static final String PLAY_COOKIE = "play.mvc.Http$Cookie";
    private static final String PLAY_COOKIE_BUILDER = "play.mvc.Http$CookieBuilder";
    private static final String SPRING_BOOT_COOKIE = "org.springframework.boot.web.server.Cookie";
    private static final String SPRING_HTTP_COOKIE_BUILDER = "org.springframework.http.ResponseCookie$ResponseCookieBuilder";
    private static final String SPRING_SECURITY_COOKIE_TOKEN_REPO = "org.springframework.security.web.csrf.CookieCsrfTokenRepository";
  }

  private static final Set SETTER_NAMES = Set.of("setHttpOnly", "withHttpOnly", "httpOnly");

  private static final Set CLASSES = Set.of(
    ClassName.SERVLET_COOKIE,
    ClassName.JAKARTA_SERVLET_COOKIE,
    ClassName.NET_HTTP_COOKIE,
    ClassName.JAX_RS_COOKIE,
    ClassName.JAKARTA_RS_COOKIE,
    ClassName.SHIRO_COOKIE,
    ClassName.PLAY_COOKIE,
    ClassName.PLAY_COOKIE_BUILDER,
    ClassName.SPRING_BOOT_COOKIE,
    ClassName.SPRING_HTTP_COOKIE_BUILDER);

  private static final MethodMatchers PLAY_COOKIE_BUILDER = MethodMatchers.create()
    .ofTypes(ClassName.PLAY_COOKIE).names("builder").withAnyParameters().build();

  private static final MethodMatchers CONSTRUCTORS_WITH_HTTP_ONLY_PARAM = MethodMatchers.or(
    MethodMatchers.create()
      .ofSubTypes(ClassName.JAX_RS_NEW_COOKIE)
      .constructor()
      .addParametersMatcher(ClassName.JAX_RS_COOKIE, JAVA_LANG_STRING, INT, JAVA_UTIL_DATE, BOOLEAN, BOOLEAN)
      .build(),
    MethodMatchers.create()
      .ofSubTypes(ClassName.JAKARTA_RS_NEW_COOKIE)
      .constructor()
      .addParametersMatcher(ClassName.JAKARTA_RS_COOKIE, JAVA_LANG_STRING, INT, JAVA_UTIL_DATE, BOOLEAN, BOOLEAN)
      .build(),
    MethodMatchers.create()
      .ofSubTypes(ClassName.JAX_RS_NEW_COOKIE, ClassName.JAKARTA_RS_NEW_COOKIE)
      .constructor()
      .addParametersMatcher(JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING, INT, JAVA_LANG_STRING, INT, JAVA_UTIL_DATE, BOOLEAN, BOOLEAN)
      .build(),
    MethodMatchers.create()
      .ofSubTypes(ClassName.JAX_RS_NEW_COOKIE, ClassName.JAKARTA_RS_NEW_COOKIE)
      .constructor()
      .addParametersMatcher(JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING, INT, BOOLEAN, BOOLEAN)
      .build(),
    MethodMatchers.create()
      .ofSubTypes(ClassName.PLAY_COOKIE, ClassName.JAKARTA_RS_NEW_COOKIE)
      .constructor()
      .addParametersMatcher(JAVA_LANG_STRING, JAVA_LANG_STRING, "java.lang.Integer", JAVA_LANG_STRING, JAVA_LANG_STRING, BOOLEAN, BOOLEAN)
      .build());

  private static final MethodMatchers CONSTRUCTORS_WITH_GOOD_DEFAULT = MethodMatchers.create()
    .ofSubTypes(ClassName.SHIRO_COOKIE)
    .constructor()
    .addWithoutParametersMatcher()
    .addParametersMatcher(JAVA_LANG_STRING)
    .build();

  private static final MethodMatchers CONSTRUCTORS_WITH_HTTP_ONLY_PARAM_BEFORE_LAST = MethodMatchers.create()
    .ofTypes(ClassName.PLAY_COOKIE)
    .constructor()
    .addParametersMatcher(JAVA_LANG_STRING, JAVA_LANG_STRING, "java.lang.Integer", JAVA_LANG_STRING, JAVA_LANG_STRING, BOOLEAN, BOOLEAN, "play.mvc.Http$Cookie$SameSite")
    .build();

  private static final MethodMatchers UNALLOWED_TOKEN_PROVIDERS = MethodMatchers.or(
    MethodMatchers.create()
      .ofTypes(ClassName.SPRING_SECURITY_COOKIE_TOKEN_REPO)
      .names("withHttpOnlyFalse")
      .addWithoutParametersMatcher()
      .build());

  @Override
  public void setContext(JavaFileScannerContext context) {
    ignoredVariables.clear();
    symbolConstructorMapToReport.clear();
    settersToReport.clear();
    newClassToReport.clear();
    super.setContext(context);
  }

  @Override
  public void leaveFile(JavaFileScannerContext context) {
    for (TypeTree typeTree : symbolConstructorMapToReport.values()) {
      reportIssue(typeTree, MESSAGE);
    }
    for (MethodInvocationTree mit : settersToReport) {
      reportIssue(mit.arguments(), MESSAGE);
    }
    for (TypeTree typeTree : newClassToReport) {
      reportIssue(typeTree, MESSAGE);
    }
  }

  @Override
  public List nodesToVisit() {
    return Arrays.asList(
      Tree.Kind.VARIABLE,
      Tree.Kind.ASSIGNMENT,
      Tree.Kind.METHOD_INVOCATION,
      Tree.Kind.RETURN_STATEMENT);
  }

  @Override
  public void visitNode(Tree tree) {
    if (tree.is(Tree.Kind.VARIABLE)) {
      checkVariableDeclaration((VariableTree) tree);
    } else if (tree.is(Tree.Kind.ASSIGNMENT)) {
      checkAssignment((AssignmentExpressionTree) tree);
    } else if (tree.is(Tree.Kind.METHOD_INVOCATION)) {
      MethodInvocationTree invocationTree = (MethodInvocationTree) tree;
      checkInvocation(invocationTree);
      invocationTree.arguments().forEach(this::categorizeBasedOnConstructor);
    } else {
      categorizeBasedOnConstructor(((ReturnStatementTree) tree).expression());
    }
  }

  private void checkAssignment(AssignmentExpressionTree assignment) {
    checkCookieBuilder(assignment);
    if (shouldVerify(assignment)) {
      categorizeBasedOnConstructor((NewClassTree) assignment.expression(),
        (VariableSymbol) ((IdentifierTree) assignment.variable()).symbol());
    }
  }

  private void checkVariableDeclaration(VariableTree declaration) {
    checkCookieBuilder(declaration);
    if (shouldVerify(declaration)) {
      categorizeBasedOnConstructor((NewClassTree) declaration.initializer(),
        (VariableSymbol) declaration.symbol());
    }
  }

  private void checkCookieBuilder(AssignmentExpressionTree assignment) {
    if (assignment.expression().is(Tree.Kind.METHOD_INVOCATION)
      && (assignment.variable().is(Tree.Kind.IDENTIFIER) || assignment.variable().is(Tree.Kind.MEMBER_SELECT))) {
      MethodInvocationTree mit = (MethodInvocationTree) assignment.expression();
      VariableSymbol variableSymbol = getVariableSymbol(assignment);
      if (variableSymbol != null) {
        addToIgnoredVariables(variableSymbol, mit);
      }
    }
  }

  @CheckForNull
  private static VariableSymbol getVariableSymbol(AssignmentExpressionTree assignment) {
    VariableSymbol variableSymbol = null;
    if (assignment.variable().is(Tree.Kind.IDENTIFIER)) {
      Symbol reference = ((IdentifierTree) assignment.variable()).symbol();
      if (reference.isVariableSymbol()) {
        variableSymbol = (VariableSymbol) reference;
      }
    } else {
      MemberSelectExpressionTree mse = (MemberSelectExpressionTree) assignment.variable();
      if (mse.identifier().symbol().isVariableSymbol()) {
        variableSymbol = (VariableSymbol) mse.identifier().symbol();
      }
    }
    return variableSymbol;
  }

  private void addToIgnoredVariables(VariableSymbol variableSymbol, MethodInvocationTree mit) {
    if (PLAY_COOKIE_BUILDER.matches(mit) && isIgnoredCookieName(mit.arguments())) {
      ignoredVariables.add(variableSymbol);
    }
  }

  private void checkCookieBuilder(VariableTree declaration) {
    Symbol symbol = declaration.symbol();
    if (!symbol.isVariableSymbol()) {
      // might happen in context of lambda, where symbol of variable cannot be resolve
      return;
    }
    ExpressionTree initializer = declaration.initializer();
    if (initializer != null && initializer.is(Tree.Kind.METHOD_INVOCATION)) {
      MethodInvocationTree mit = (MethodInvocationTree) initializer;
      addToIgnoredVariables((VariableSymbol) symbol, mit);
    }
  }

  private void categorizeBasedOnConstructor(@Nullable ExpressionTree expressionTree) {
    if (expressionTree != null && expressionTree.is(Tree.Kind.NEW_CLASS)) {
      NewClassTree newClass = (NewClassTree) expressionTree;
      if (!isIgnoredCookieName(newClass.arguments()) && !isCompliantConstructorCall(newClass) && CLASSES.stream().anyMatch(newClass.symbolType()::isSubtypeOf)) {
        newClassToReport.add(newClass.identifier());
      }
    }
  }

  private void categorizeBasedOnConstructor(NewClassTree newClassTree, VariableSymbol variableSymbol) {
    if (isIgnoredCookieName(newClassTree.arguments())) {
      ignoredVariables.add(variableSymbol);
    } else if (!isCompliantConstructorCall(newClassTree)) {
      symbolConstructorMapToReport.put(variableSymbol, newClassTree.identifier());
    }
  }

  private static boolean shouldVerify(VariableTree variableDeclaration) {
    ExpressionTree initializer = variableDeclaration.initializer();
    if (initializer != null && initializer.is(Tree.Kind.NEW_CLASS)) {
      boolean isSupportedClass = CLASSES.stream().anyMatch(variableDeclaration.type().symbolType()::isSubtypeOf)
        || CLASSES.stream().anyMatch(initializer.symbolType()::isSubtypeOf);
      return variableDeclaration.symbol().owner().isMethodSymbol() && isSupportedClass;
    }
    return false;
  }

  private static boolean shouldVerify(AssignmentExpressionTree assignment) {
    if (assignment.expression().is(Tree.Kind.NEW_CLASS) && assignment.variable().is(Tree.Kind.IDENTIFIER)) {
      IdentifierTree identifier = (IdentifierTree) assignment.variable();
      boolean isMethodVariable = identifier.symbol().isVariableSymbol()
        && identifier.symbol().owner().isMethodSymbol();
      boolean isSupportedClass = CLASSES.stream().anyMatch(identifier.symbolType()::isSubtypeOf)
        || CLASSES.stream().anyMatch(assignment.expression().symbolType()::isSubtypeOf);
      return isMethodVariable && isSupportedClass;
    }
    return false;
  }

  private static boolean isCompliantConstructorCall(NewClassTree newClassTree) {
    Arguments arguments = newClassTree.arguments();
    if (CONSTRUCTORS_WITH_HTTP_ONLY_PARAM.matches(newClassTree)) {
      ExpressionTree lastArgument = arguments.get(arguments.size() - 1);
      return LiteralUtils.isTrue(lastArgument);
    } else if (CONSTRUCTORS_WITH_HTTP_ONLY_PARAM_BEFORE_LAST.matches(newClassTree)) {
      ExpressionTree beforeLastArgument = arguments.get(arguments.size() - 2);
      return LiteralUtils.isTrue(beforeLastArgument);
    } else {
      return CONSTRUCTORS_WITH_GOOD_DEFAULT.matches(newClassTree);
    }
  }

  private static boolean isIgnoredCookieName(Arguments arguments) {
    if (arguments.isEmpty()) {
      return false;
    }
    ExpressionTree nameArgument = arguments.get(COOKIE_NAME_ARGUMENT);
    String name = ExpressionsHelper.getConstantValueAsString(nameArgument).value();
    return name != null && IGNORED_COOKIE_NAMES.stream().anyMatch(cookieName -> name.toLowerCase(Locale.ENGLISH).contains(cookieName));
  }

  private void checkInvocation(MethodInvocationTree mit) {
    if(UNALLOWED_TOKEN_PROVIDERS.matches(mit)) {
      settersToReport.add(mit);
    } else if (isExpectedSetter(mit)) {
      if (mit.methodSelect().is(Tree.Kind.MEMBER_SELECT)) {
        ExpressionTree expression = ((MemberSelectExpressionTree) mit.methodSelect()).expression();
        boolean isCalledOnIdentifier = expression.is(Tree.Kind.IDENTIFIER);
        boolean isCalledOnMemberSelect = expression.is(Tree.Kind.MEMBER_SELECT);
        if (isCalledOnIdentifier || isCalledOnMemberSelect) {
          updateIssuesToReport(mit);
        } else if (!setterArgumentHasCompliantValue(mit.arguments())) {
          // builder method
          settersToReport.add(mit);
        }
      } else if (!setterArgumentHasCompliantValue(mit.arguments())) {
        // sub-class method
        settersToReport.add(mit);
      }
    }
  }

  private static boolean isExpectedSetter(MethodInvocationTree mit) {
    return mit.arguments().size() == 1
      && mit.methodSymbol().isMethodSymbol()
      && CLASSES.stream().anyMatch(mit.methodSymbol().owner().type()::isSubtypeOf)
      && SETTER_NAMES.contains(getIdentifier(mit).name())
      && isIgnoredBuilder(mit);
  }

  private static boolean isIgnoredBuilder(MethodInvocationTree mit) {
    if (!mit.methodSymbol().owner().type().isSubtypeOf(ClassName.PLAY_COOKIE_BUILDER)) {
      return true;
    }
    return getMethodChain(mit)
      .filter(method -> "builder".contains(getIdentifier(method).name()))
      .noneMatch(method -> isIgnoredCookieName(method.arguments()));
  }

  private static Stream getMethodChain(MethodInvocationTree mit) {
    if (mit.methodSelect().is(Tree.Kind.MEMBER_SELECT)) {
      ExpressionTree expressionTree = ((MemberSelectExpressionTree) mit.methodSelect()).expression();
      if (expressionTree.is(Tree.Kind.METHOD_INVOCATION)) {
        return Stream.concat(Stream.of(mit), getMethodChain((MethodInvocationTree) expressionTree));
      }
    }
    return Stream.of(mit);
  }

  private void updateIssuesToReport(MethodInvocationTree mit) {
    MemberSelectExpressionTree mse = (MemberSelectExpressionTree) mit.methodSelect();
    VariableSymbol reference;
    if (mse.expression().is(Tree.Kind.IDENTIFIER)) {
      reference = (VariableSymbol) ((IdentifierTree) mse.expression()).symbol();
    } else {
      reference = (VariableSymbol) ((MemberSelectExpressionTree) mse.expression()).identifier().symbol();
    }
    if (ignoredVariables.contains(reference)) {
      // ignore XSRF-TOKEN cookies
      return;
    }
    symbolConstructorMapToReport.remove(reference);
    if (!setterArgumentHasCompliantValue(mit.arguments())) {
      settersToReport.add(mit);
    }
  }

  private static boolean setterArgumentHasCompliantValue(Arguments arguments) {
    ExpressionTree expressionTree = arguments.get(0);
    Boolean booleanValue = ExpressionsHelper.getConstantValueAsBoolean(expressionTree).value();
    return booleanValue == null || booleanValue;
  }

  private static IdentifierTree getIdentifier(MethodInvocationTree mit) {
    IdentifierTree id;
    if (mit.methodSelect().is(Tree.Kind.IDENTIFIER)) {
      id = (IdentifierTree) mit.methodSelect();
    } else {
      id = ((MemberSelectExpressionTree) mit.methodSelect()).identifier();
    }
    return id;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy