/**
* Find Security Bugs
* Copyright (c) Philippe Arteau, All rights reserved.
*
* This library 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.0 of the License, or (at your option) any later version.
*
* This library 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 library.
*/
package com.h3xstream.findsecbugs.csrf;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.Detector;
import edu.umd.cs.findbugs.Priorities;
import edu.umd.cs.findbugs.ba.ClassContext;
import org.apache.bcel.classfile.AnnotationEntry;
import org.apache.bcel.classfile.ArrayElementValue;
import org.apache.bcel.classfile.ElementValue;
import org.apache.bcel.classfile.ElementValuePair;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import java.util.Arrays;
import java.util.List;
/**
* Detects Spring CSRF unrestricted RequestMapping
*
* @author Pablo Tamarit
*/
public class SpringCsrfUnrestrictedRequestMappingDetector implements Detector {
private static final String SPRING_CSRF_UNRESTRICTED_REQUEST_MAPPING_TYPE = "SPRING_CSRF_UNRESTRICTED_REQUEST_MAPPING";
private static final String REQUEST_MAPPING_ANNOTATION_TYPE = "Lorg/springframework/web/bind/annotation/RequestMapping;";
private static final String METHOD_ANNOTATION_ATTRIBUTE_KEY = "method";
private static final List<String> UNPROTECTED_HTTP_REQUEST_METHODS = Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS");
private BugReporter bugReporter;
public SpringCsrfUnrestrictedRequestMappingDetector(BugReporter bugReporter) {
this.bugReporter = bugReporter;
}
@Override
public void visitClassContext(ClassContext classContext) {
JavaClass javaClass = classContext.getJavaClass();
for (Method method : javaClass.getMethods()) {
if (isVulnerable(method)) {
bugReporter.reportBug(new BugInstance(this, SPRING_CSRF_UNRESTRICTED_REQUEST_MAPPING_TYPE, Priorities.HIGH_PRIORITY) //
.addClassAndMethod(javaClass, method));
}
}
}
@Override
public void report() {
}
private static boolean isVulnerable(Method method) {
// If the method is not annotated with `@RequestMapping`, there is no vulnerability.
AnnotationEntry requestMappingAnnotation = findRequestMappingAnnotation(method);
if (requestMappingAnnotation == null) {
return false;
}
// If the `@RequestMapping` annotation is used without the `method` annotation attribute,
// there is a vulnerability.
ElementValuePair methodAnnotationAttribute = findMethodAnnotationAttribute(requestMappingAnnotation);
if (methodAnnotationAttribute == null) {
return true;
}
// If the `@RequestMapping` annotation is used with the `method` annotation attribute equal to `{}`,
// there is a vulnerability.
ElementValue methodAnnotationAttributeValue = methodAnnotationAttribute.getValue();
if (isEmptyArray(methodAnnotationAttributeValue)) {
return true;
}
// If the `@RequestMapping` annotation is used with the `method` annotation attribute but contains a mix of
// unprotected and protected HTTP request methods, there is a vulnerability.
return isMixOfUnprotectedAndProtectedHttpRequestMethods(methodAnnotationAttributeValue);
}
private static AnnotationEntry findRequestMappingAnnotation(Method method) {
for (AnnotationEntry annotationEntry : method.getAnnotationEntries()) {
if (REQUEST_MAPPING_ANNOTATION_TYPE.equals(annotationEntry.getAnnotationType())) {
return annotationEntry;
}
}
return null;
}
private static ElementValuePair findMethodAnnotationAttribute(AnnotationEntry requestMappingAnnotation) {
for (ElementValuePair elementValuePair : requestMappingAnnotation.getElementValuePairs()) {
if (METHOD_ANNOTATION_ATTRIBUTE_KEY.equals(elementValuePair.getNameString())) {
return elementValuePair;
}
}
return null;
}
private static boolean isEmptyArray(ElementValue methodAnnotationAttributeValue) {
if (!(methodAnnotationAttributeValue instanceof ArrayElementValue)) {
return false;
}
ArrayElementValue arrayElementValue = (ArrayElementValue) methodAnnotationAttributeValue;
return arrayElementValue.getElementValuesArraySize() == 0;
}
private static boolean isMixOfUnprotectedAndProtectedHttpRequestMethods(ElementValue methodAnnotationAttributeValue) {
if (!(methodAnnotationAttributeValue instanceof ArrayElementValue)) {
return false;
}
ArrayElementValue arrayElementValue = (ArrayElementValue) methodAnnotationAttributeValue;
// There cannot be a mix if there is no more than one element.
if (arrayElementValue.getElementValuesArraySize() <= 1) {
return false;
}
// Return `true` as soon as we find at least one unprotected and at least one protected HTTP request method.
boolean atLeastOneUnprotected = false;
boolean atLeastOneProtected = false;
ElementValue[] elementValues = arrayElementValue.getElementValuesArray();
for (ElementValue elementValue : elementValues) {
if (UNPROTECTED_HTTP_REQUEST_METHODS.contains(elementValue.stringifyValue())) {
atLeastOneUnprotected = true;
} else {
atLeastOneProtected = true;
}
if (atLeastOneUnprotected && atLeastOneProtected) {
return true;
}
}
return false;
}
}