org.sonar.java.checks.security.CookieHttpOnlyCheck Maven / Gradle / Ivy
/*
* SonarQube Java
* Copyright (C) 2012-2018 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 GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* 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 GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.java.checks.security;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;
import javax.annotation.CheckForNull;
import org.sonar.check.Rule;
import org.sonar.java.checks.helpers.ConstantUtils;
import org.sonar.java.checks.helpers.IdentifierUtils;
import org.sonar.java.matcher.MethodMatcher;
import org.sonar.java.matcher.TypeCriteria;
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.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.VariableTree;
@Rule(key = "S3330")
public class CookieHttpOnlyCheck extends IssuableSubscriptionVisitor {
private final List compliantConstructorInitializations = Lists.newArrayList();
private final List ignoredVariables = Lists.newArrayList();
private final List variablesToReport = Lists.newArrayList();
private final List settersToReport = Lists.newArrayList();
private final List newClassToReport = Lists.newArrayList();
private static final List IGNORED_COOKIE_NAMES = ImmutableList.of("csrf", "xsrf");
private static final String CONSTRUCTOR = "";
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 = "Add the \"HttpOnly\" cookie attribute.";
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 NET_HTTP_COOKIE = "java.net.HttpCookie";
private static final String JAX_RS_COOKIE = "javax.ws.rs.core.Cookie";
private static final String JAX_RS_NEW_COOKIE = "javax.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 List SETTER_NAMES = Arrays.asList("setHttpOnly", "withHttpOnly");
private static final List CLASSES = Arrays.asList(
ClassName.SERVLET_COOKIE,
ClassName.NET_HTTP_COOKIE,
ClassName.JAX_RS_COOKIE,
ClassName.SHIRO_COOKIE,
ClassName.PLAY_COOKIE,
ClassName.PLAY_COOKIE_BUILDER);
private static final MethodMatcher PLAY_COOKIE_BUILDER = MethodMatcher.create()
.typeDefinition(ClassName.PLAY_COOKIE).name("builder").withAnyParameters();
private static final List CONSTRUCTORS_WITH_HTTP_ONLY_PARAM = Arrays.asList(
MethodMatcher.create()
.typeDefinition(TypeCriteria.subtypeOf(ClassName.JAX_RS_NEW_COOKIE)).name(CONSTRUCTOR)
.parameters(ClassName.JAX_RS_COOKIE, JAVA_LANG_STRING, INT, JAVA_UTIL_DATE, BOOLEAN, BOOLEAN),
MethodMatcher.create()
.typeDefinition(TypeCriteria.subtypeOf(ClassName.JAX_RS_NEW_COOKIE)).name(CONSTRUCTOR)
.parameters(JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING, INT, JAVA_LANG_STRING, INT, JAVA_UTIL_DATE, BOOLEAN, BOOLEAN),
MethodMatcher.create()
.typeDefinition(TypeCriteria.subtypeOf(ClassName.JAX_RS_NEW_COOKIE)).name(CONSTRUCTOR)
.parameters(JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING, INT, BOOLEAN, BOOLEAN),
MethodMatcher.create()
.typeDefinition(TypeCriteria.subtypeOf(ClassName.PLAY_COOKIE)).name(CONSTRUCTOR)
.parameters(JAVA_LANG_STRING, JAVA_LANG_STRING, "java.lang.Integer", JAVA_LANG_STRING, JAVA_LANG_STRING, BOOLEAN, BOOLEAN));
private static final List CONSTRUCTORS_WITH_GOOD_DEFAULT = Arrays.asList(
MethodMatcher.create().typeDefinition(TypeCriteria.subtypeOf(ClassName.SHIRO_COOKIE)).name(CONSTRUCTOR).withoutParameter(),
MethodMatcher.create().typeDefinition(TypeCriteria.subtypeOf(ClassName.SHIRO_COOKIE)).name(CONSTRUCTOR).parameters(JAVA_LANG_STRING));
@Override
public void scanFile(JavaFileScannerContext context) {
compliantConstructorInitializations.clear();
ignoredVariables.clear();
variablesToReport.clear();
settersToReport.clear();
newClassToReport.clear();
super.scanFile(context);
for (VariableSymbol var : variablesToReport) {
VariableTree declaration = var.declaration();
if (declaration != null) {
reportIssue(declaration.simpleName(), MESSAGE);
}
}
for (MethodInvocationTree mit : settersToReport) {
reportIssue(mit.arguments(), MESSAGE);
}
for (NewClassTree newClassTree : newClassToReport) {
reportIssue(newClassTree, MESSAGE);
}
}
@Override
public List nodesToVisit() {
return ImmutableList.of(
Tree.Kind.VARIABLE,
Tree.Kind.ASSIGNMENT,
Tree.Kind.METHOD_INVOCATION,
Tree.Kind.RETURN_STATEMENT);
}
@Override
public void visitNode(Tree tree) {
if (hasSemantic()) {
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)) {
checkSetterInvocation((MethodInvocationTree) tree);
} else {
categorizeBasedOnConstructor((ReturnStatementTree) tree);
}
}
}
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) {
if (declaration.initializer() != null && declaration.initializer().is(Tree.Kind.METHOD_INVOCATION)) {
MethodInvocationTree mit = (MethodInvocationTree) declaration.initializer();
addToIgnoredVariables((VariableSymbol) declaration.symbol(), mit);
}
}
private void categorizeBasedOnConstructor(ReturnStatementTree returnStatement) {
ExpressionTree returnedExpression = returnStatement.expression();
if (returnedExpression != null && returnedExpression.is(Tree.Kind.NEW_CLASS)) {
NewClassTree newClass = (NewClassTree) returnedExpression;
if (!isIgnoredCookieName(newClass.arguments()) && !isCompliantConstructorCall(newClass) && CLASSES.stream().anyMatch(newClass.symbolType()::isSubtypeOf)) {
newClassToReport.add(newClass);
}
}
}
private void categorizeBasedOnConstructor(NewClassTree newClassTree, VariableSymbol variableSymbol) {
if (isIgnoredCookieName(newClassTree.arguments())) {
ignoredVariables.add(variableSymbol);
} else if (isCompliantConstructorCall(newClassTree)) {
compliantConstructorInitializations.add(variableSymbol);
} else {
variablesToReport.add(variableSymbol);
}
}
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) {
if (CONSTRUCTORS_WITH_HTTP_ONLY_PARAM.stream().anyMatch(matcher -> matcher.matches(newClassTree))) {
Arguments arguments = newClassTree.arguments();
ExpressionTree lastArgument = arguments.get(arguments.size() - 1);
return LiteralUtils.isTrue(lastArgument);
} else {
return CONSTRUCTORS_WITH_GOOD_DEFAULT.stream().anyMatch(matcher -> matcher.matches(newClassTree));
}
}
private static boolean isIgnoredCookieName(Arguments arguments) {
if (arguments.isEmpty()) {
return false;
}
ExpressionTree nameArgument = arguments.get(COOKIE_NAME_ARGUMENT);
String name = IdentifierUtils.getValue(nameArgument, ConstantUtils::resolveAsStringConstant);
return name != null && IGNORED_COOKIE_NAMES.stream().anyMatch(cookieName -> name.toLowerCase(Locale.ENGLISH).contains(cookieName));
}
private void checkSetterInvocation(MethodInvocationTree mit) {
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.symbol().isMethodSymbol()
&& CLASSES.stream().anyMatch(mit.symbol().owner().type()::isSubtypeOf)
&& SETTER_NAMES.contains(getIdentifier(mit).name())
&& isIgnoredBuilder(mit);
}
private static boolean isIgnoredBuilder(MethodInvocationTree mit) {
if (!mit.symbol().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;
}
if (setterArgumentHasCompliantValue(mit.arguments())) {
variablesToReport.remove(reference);
} else if (compliantConstructorInitializations.contains(reference)) {
variablesToReport.add(reference);
} else if (!variablesToReport.contains(reference)) {
settersToReport.add(mit);
}
}
private static boolean setterArgumentHasCompliantValue(Arguments arguments) {
ExpressionTree expressionTree = arguments.get(0);
Boolean booleanValue = IdentifierUtils.getValue(expressionTree, ConstantUtils::resolveAsBooleanConstant);
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