
org.sonar.java.checks.security.ExcessiveContentRequestCheck Maven / Gradle / Ivy
/*
* 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.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.check.Rule;
import org.sonar.check.RuleProperty;
import org.sonar.java.AnalysisException;
import org.sonar.java.model.DefaultJavaFileScannerContext;
import org.sonar.java.model.DefaultModuleScannerContext;
import org.sonar.java.model.ExpressionUtils;
import org.sonar.java.reporting.AnalyzerMessage;
import org.sonar.plugins.java.api.InputFileScannerContext;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.ModuleScannerContext;
import org.sonar.plugins.java.api.caching.CacheContext;
import org.sonar.plugins.java.api.caching.JavaReadCache;
import org.sonar.plugins.java.api.caching.JavaWriteCache;
import org.sonar.plugins.java.api.internal.EndOfAnalysis;
import org.sonar.plugins.java.api.semantic.MethodMatchers;
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.Tree;
import static org.sonar.java.checks.security.ExcessiveContentRequestCheck.CachedResult.toBytes;
import static org.sonar.plugins.java.api.semantic.MethodMatchers.ANY;
@Rule(key = "S5693")
public class ExcessiveContentRequestCheck extends IssuableSubscriptionVisitor implements EndOfAnalysis {
@RuleProperty(
key = "fileUploadSizeLimit",
description = "The maximum size of HTTP requests handling file uploads (in bytes).",
defaultValue = "" + DEFAULT_MAX)
public long fileUploadSizeLimit = DEFAULT_MAX;
private static final long BYTES_PER_KB = 1_024L;
private static final long BYTES_PER_MB = 1_048_576L;
private static final long BYTES_PER_GB = 1_073_741_824L;
private static final long BYTES_PER_TB = 1_099_511_627_776L;
private static final long DEFAULT_MAX = 8 * BYTES_PER_MB;
private static final String MESSAGE_EXCEED_SIZE = "The content length limit of %d bytes is greater than the defined limit of %d; make sure it is safe here.";
private static final String MESSAGE_SIZE_NOT_SET = "Make sure not setting any maximum content length limit is safe here.";
private static final String DATA_SIZE = "org.springframework.util.unit.DataSize";
private static final Pattern DATA_SIZE_PATTERN = Pattern.compile("^([+\\-]?\\d+)([a-zA-Z]{0,2})$");
private static final String MULTIPART_RESOLVER = "org.springframework.web.multipart.commons.CommonsMultipartResolver";
private static final String MULTIPART_CONFIG = "org.springframework.boot.web.servlet.MultipartConfigFactory";
private static final String MULTIPART_PROPERTIES = "org.springframework.boot.autoconfigure.web.servlet.MultipartProperties";
private static final MethodMatchers METHODS_SETTING_MAX_SIZE = MethodMatchers.or(
MethodMatchers.create()
.ofSubTypes(MULTIPART_RESOLVER)
.names("setMaxUploadSize")
.addParametersMatcher("long")
.build(),
MethodMatchers.create()
.ofSubTypes(MULTIPART_CONFIG)
.names("setMaxFileSize", "setMaxRequestSize")
.addParametersMatcher("long")
.addParametersMatcher("java.lang.String")
.build(),
MethodMatchers.create()
.ofSubTypes(MULTIPART_PROPERTIES)
.names("setMaxFileSize", "setMaxRequestSize")
.addParametersMatcher(DATA_SIZE)
.build()
);
private static final MethodMatchers MULTIPART_CONSTRUCTOR = MethodMatchers.create()
.ofSubTypes(MULTIPART_RESOLVER, MULTIPART_CONFIG)
.constructor()
.withAnyParameters()
.build();
private static final MethodMatchers DATA_SIZE_OF_SOMETHING = MethodMatchers.create()
.ofSubTypes(DATA_SIZE)
.name(name -> name.startsWith("of"))
.addParametersMatcher("long")
.build();
private static final MethodMatchers DATA_SIZE_WITH_UNIT = MethodMatchers.create()
.ofSubTypes(DATA_SIZE)
.names("parse", "of")
.addParametersMatcher(ANY, "org.springframework.util.unit.DataUnit")
.build();
private static final MethodMatchers DATA_SIZE_PARSE = MethodMatchers.create()
.ofSubTypes(DATA_SIZE)
.names("parse")
.addParametersMatcher("java.lang.CharSequence")
.build();
public static final String CACHE_KEY_CACHED = "java:S5693:cached";
public static final String CACHE_KEY_INSTANTIATE = "java:S5693:instantiate";
public static final String CACHE_KEY_SET_MAXIMUM_SIZE = "java:S5693:maximumSize";
private static final Logger LOGGER = LoggerFactory.getLogger(ExcessiveContentRequestCheck.class);
private final List multipartConstructorIssues = new ArrayList<>();
private boolean sizeSetSomewhere = false;
private Set filesCached = new HashSet<>();
private boolean currentFileSetsMaximumSize = false;
private boolean currentFileInstantiates = false;
@Override
public boolean scanWithoutParsing(InputFileScannerContext context) {
InputFile unchangedFile = context.getInputFile();
CacheContext cacheContext = context.getCacheContext();
// Check if results have been cached previously for this unchanged file
Optional cachedEntry = loadFromPreviousAnalysis(cacheContext, unchangedFile);
if (cachedEntry.isEmpty()) {
LOGGER.trace("No cached data for rule java:S5693 on file {}", unchangedFile);
return false;
}
boolean inputFileSetsMaximumSize = cachedEntry.get().setMaximumSize;
if (inputFileSetsMaximumSize) {
sizeSetSomewhere = true;
}
keepForNextAnalysis(cacheContext, context.getInputFile());
filesCached.add(unchangedFile.key());
return true;
}
@Override
public void endOfAnalysis(ModuleScannerContext context) {
if (!sizeSetSomewhere) {
var defaultContext = (DefaultModuleScannerContext) context;
multipartConstructorIssues.forEach(defaultContext::reportIssue);
}
filesCached.clear();
multipartConstructorIssues.clear();
sizeSetSomewhere = false;
}
@Override
public List nodesToVisit() {
return Arrays.asList(Tree.Kind.METHOD_INVOCATION, Tree.Kind.NEW_CLASS);
}
@Override
public void visitNode(Tree tree) {
DefaultJavaFileScannerContext defaultContext = (DefaultJavaFileScannerContext) context;
if (tree.is(Tree.Kind.NEW_CLASS)) {
NewClassTree newClassTree = (NewClassTree) tree;
if (MULTIPART_CONSTRUCTOR.matches(newClassTree)) {
// Create an issue that we will report only at the end of the analysis if the maximum size was never set.
AnalyzerMessage analyzerMessage = defaultContext.createAnalyzerMessage(this, newClassTree, MESSAGE_SIZE_NOT_SET);
multipartConstructorIssues.add(analyzerMessage);
currentFileInstantiates = true;
}
} else {
MethodInvocationTree mit = (MethodInvocationTree) tree;
if (METHODS_SETTING_MAX_SIZE.matches(mit)) {
currentFileSetsMaximumSize = true;
sizeSetSomewhere = true;
getIfExceedSize(mit.arguments().get(0))
.map(bytesExceeding -> defaultContext.createAnalyzerMessage(this, mit, String.format(MESSAGE_EXCEED_SIZE, bytesExceeding, fileUploadSizeLimit)))
.ifPresent(defaultContext::reportIssue);
}
}
}
@Override
public void leaveFile(JavaFileScannerContext context) {
super.leaveFile(context);
CacheContext cacheContext = context.getCacheContext();
if (cacheContext.isCacheEnabled()) {
writeForNextAnalysis(cacheContext, context.getInputFile(), currentFileInstantiates, currentFileSetsMaximumSize);
}
currentFileSetsMaximumSize = false;
currentFileInstantiates = false;
}
private Optional getIfExceedSize(ExpressionTree expressionTree) {
if (expressionTree.is(Tree.Kind.METHOD_INVOCATION)) {
return getSizeFromDataSize((MethodInvocationTree) expressionTree)
.filter(b -> b > fileUploadSizeLimit);
}
return getNumberOfBytes(expressionTree).filter(b -> b > fileUploadSizeLimit);
}
private static Optional getSizeFromDataSize(MethodInvocationTree mit) {
if (DATA_SIZE_PARSE.matches(mit)) {
return getNumberOfBytes(mit.arguments().get(0));
} else if (DATA_SIZE_OF_SOMETHING.matches(mit)) {
return getNumberOfBytes(mit.arguments().get(0))
.map(b -> b * getMultiplierFromName(ExpressionUtils.methodName(mit).name()));
} else if (DATA_SIZE_WITH_UNIT.matches(mit)) {
Optional multiplier = getIdentifierName(mit.arguments().get(1))
.map(ExcessiveContentRequestCheck::getMultiplierFromName);
if (multiplier.isPresent()) {
return getNumberOfBytes(mit.arguments().get(0))
.map(l -> l * multiplier.get());
}
}
return Optional.empty();
}
private static Optional getNumberOfBytes(ExpressionTree expression) {
Optional integerOptional = expression.asConstant(Integer.class);
if (integerOptional.isPresent()) {
return Optional.of(integerOptional.get().longValue());
}
Optional stringOptional = expression.asConstant(String.class);
if (stringOptional.isPresent()) {
return getLongValueFromString(stringOptional.get());
}
return expression.asConstant(Long.class);
}
private static Optional getLongValueFromString(String s) {
Matcher matcher = DATA_SIZE_PATTERN.matcher(s);
if (matcher.matches()) {
return Optional.of(Long.parseLong(matcher.group(1)) * getMultiplierFromName(matcher.group(2)));
}
return Optional.empty();
}
private static Long getMultiplierFromName(String name) {
switch (name.toUpperCase(Locale.ENGLISH)) {
case "OFKILOBYTES",
"KILOBYTES",
"KB":
return BYTES_PER_KB;
case "OFMEGABYTES",
"MEGABYTES",
"MB":
return BYTES_PER_MB;
case "OFGIGABYTES",
"GIGABYTES",
"GB":
return BYTES_PER_GB;
case "OFTERABYTES",
"TERABYTES",
"TB":
return BYTES_PER_TB;
default:
return 1L;
}
}
private static Optional getIdentifierName(ExpressionTree expression) {
if (expression.is(Tree.Kind.IDENTIFIER)) {
return Optional.of(((IdentifierTree) expression).name());
} else if (expression.is(Tree.Kind.MEMBER_SELECT)) {
return Optional.of(((MemberSelectExpressionTree) expression).identifier().name());
}
return Optional.empty();
}
private static String computeCacheKey(InputFile inputFile) {
return "java:S5693:" + inputFile.key();
}
private static Optional loadFromPreviousAnalysis(CacheContext cacheContext, InputFile inputFile) {
JavaReadCache readCache = cacheContext.getReadCache();
String cacheKey = computeCacheKey(inputFile);
byte[] rawValue = readCache.readBytes(cacheKey);
if (rawValue == null) {
return Optional.empty();
}
try {
return Optional.ofNullable(CachedResult.fromBytes(rawValue));
} catch (IllegalArgumentException ignored) {
LOGGER.trace("Cached entry is unreadable for rule java:S5693 on file {}", inputFile);
return Optional.empty();
}
}
private static void keepForNextAnalysis(CacheContext cacheContext, InputFile inputFile) {
JavaWriteCache writeCache = cacheContext.getWriteCache();
try {
writeCache.copyFromPrevious(computeCacheKey(inputFile));
} catch (IllegalArgumentException e) {
String message = String.format("Failed to copy from previous cache for file %s", inputFile);
LOGGER.trace(message);
throw new AnalysisException(message, e);
}
}
private static void writeForNextAnalysis(CacheContext cacheContext, InputFile inputFile, boolean instantiates, boolean setsMaximumSize) {
JavaWriteCache writeCache = cacheContext.getWriteCache();
try {
writeCache.write(computeCacheKey(inputFile), toBytes(new CachedResult(instantiates, setsMaximumSize)));
} catch (IllegalArgumentException e) {
String message = String.format("Failed to write to cache for file %s", inputFile);
LOGGER.trace(message);
throw new AnalysisException(message, e);
}
}
static class CachedResult {
public static final byte INSTANTIATES_VALUE = 1;
public static final byte SETS_MAXIMUM_SIZE_VALUE = 2;
public final boolean instantiates;
public final boolean setMaximumSize;
CachedResult(boolean instantiates, boolean setMaximumSize) {
this.instantiates = instantiates;
this.setMaximumSize = setMaximumSize;
}
static CachedResult fromBytes(byte[] raw) {
if (raw.length != 1) {
throw new IllegalArgumentException(
String.format("Could not decode cached result: unexpected length (expected = 1, actual = %d)", raw.length)
);
}
return new CachedResult(
(raw[0] & INSTANTIATES_VALUE) == INSTANTIATES_VALUE,
(raw[0] & SETS_MAXIMUM_SIZE_VALUE) == SETS_MAXIMUM_SIZE_VALUE
);
}
static byte[] toBytes(CachedResult cachedResult) {
byte value = 0;
if (cachedResult.instantiates) {
value |= INSTANTIATES_VALUE;
}
if (cachedResult.setMaximumSize) {
value |= SETS_MAXIMUM_SIZE_VALUE;
}
return new byte[]{value};
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy