/*
* FindBugs - Find bugs in Java programs
* Copyright (C) 2004,2005 University of Maryland
*
* 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 2.1 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; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package edu.umd.cs.findbugs.detect;
import java.util.Iterator;
import java.util.regex.Pattern;
import javax.annotation.CheckForNull;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.AALOAD;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.GETFIELD;
import org.apache.bcel.generic.GETSTATIC;
import org.apache.bcel.generic.INVOKEINTERFACE;
import org.apache.bcel.generic.INVOKEVIRTUAL;
import org.apache.bcel.generic.Instruction;
import org.apache.bcel.generic.InstructionHandle;
import org.apache.bcel.generic.InvokeInstruction;
import org.apache.bcel.generic.LDC;
import org.apache.bcel.generic.MethodGen;
import org.apache.bcel.generic.NOP;
import org.apache.bcel.generic.Type;
import edu.umd.cs.findbugs.BugAccumulator;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.Detector;
import edu.umd.cs.findbugs.SourceLineAnnotation;
import edu.umd.cs.findbugs.SystemProperties;
import edu.umd.cs.findbugs.ba.BasicBlock;
import edu.umd.cs.findbugs.ba.CFG;
import edu.umd.cs.findbugs.ba.CFGBuilderException;
import edu.umd.cs.findbugs.ba.ClassContext;
import edu.umd.cs.findbugs.ba.DataflowAnalysisException;
import edu.umd.cs.findbugs.ba.EdgeTypes;
import edu.umd.cs.findbugs.ba.Location;
import edu.umd.cs.findbugs.ba.constant.Constant;
import edu.umd.cs.findbugs.ba.constant.ConstantDataflow;
import edu.umd.cs.findbugs.ba.constant.ConstantFrame;
import edu.umd.cs.findbugs.ba.type.TopType;
import edu.umd.cs.findbugs.ba.type.TypeDataflow;
import edu.umd.cs.findbugs.ba.type.TypeFrame;
import edu.umd.cs.findbugs.classfile.CheckedAnalysisException;
/**
* Find potential SQL injection vulnerabilities.
*
* @author David Hovemeyer
* @author Bill Pugh
* @author Matt Hargett
*/
public class FindSqlInjection implements Detector {
private static class StringAppendState {
// remember the smallest position at which we saw something that
// concerns us
int sawOpenQuote = Integer.MAX_VALUE;
int sawCloseQuote = Integer.MAX_VALUE;
int sawComma = Integer.MAX_VALUE;
int sawAppend = Integer.MAX_VALUE;
int sawUnsafeAppend = Integer.MAX_VALUE;
int sawTaint = Integer.MAX_VALUE;
int sawSeriousTaint = Integer.MAX_VALUE;
public boolean getSawOpenQuote(InstructionHandle handle) {
return sawOpenQuote <= handle.getPosition();
}
public boolean getSawCloseQuote(InstructionHandle handle) {
return sawCloseQuote <= handle.getPosition();
}
public boolean getSawComma(InstructionHandle handle) {
return sawComma <= handle.getPosition();
}
public boolean getSawAppend(InstructionHandle handle) {
return sawAppend <= handle.getPosition();
}
public boolean getSawUnsafeAppend(InstructionHandle handle) {
return sawUnsafeAppend <= handle.getPosition();
}
public boolean getSawTaint(InstructionHandle handle) {
return sawTaint <= handle.getPosition();
}
public boolean getSawSeriousTaint(InstructionHandle handle) {
return sawSeriousTaint <= handle.getPosition();
}
public void setSawOpenQuote(InstructionHandle handle) {
sawOpenQuote = Math.min(sawOpenQuote, handle.getPosition());
}
public void setSawCloseQuote(InstructionHandle handle) {
sawCloseQuote = Math.min(sawCloseQuote, handle.getPosition());
}
public void setSawComma(InstructionHandle handle) {
sawComma = Math.min(sawComma, handle.getPosition());
}
public void setSawAppend(InstructionHandle handle) {
sawAppend = Math.min(sawAppend, handle.getPosition());
}
public void setSawUnsafeAppend(InstructionHandle handle) {
sawUnsafeAppend = Math.min(sawUnsafeAppend, handle.getPosition());
}
public void setSawSeriousTaint(InstructionHandle handle) {
sawSeriousTaint = Math.min(sawSeriousTaint, handle.getPosition());
}
public void setSawTaint(InstructionHandle handle) {
sawTaint = Math.min(sawTaint, handle.getPosition());
}
public void setSawInitialTaint() {
sawTaint = 0;
}
}
BugReporter bugReporter;
BugAccumulator bugAccumulator;
private final boolean testingEnabled;
public FindSqlInjection(BugReporter bugReporter) {
this.bugReporter = bugReporter;
this.bugAccumulator = new BugAccumulator(bugReporter);
testingEnabled = SystemProperties.getBoolean("report_TESTING_pattern_in_standard_detectors");
}
@Override
public void visitClassContext(ClassContext classContext) {
JavaClass javaClass = classContext.getJavaClass();
Method[] methodList = javaClass.getMethods();
for (Method method : methodList) {
MethodGen methodGen = classContext.getMethodGen(method);
if (methodGen == null) {
continue;
}
try {
analyzeMethod(classContext, method);
} catch (DataflowAnalysisException e) {
bugReporter.logError(
"FindSqlInjection caught exception while analyzing " + classContext.getFullyQualifiedMethodName(method),
e);
} catch (CFGBuilderException e) {
bugReporter.logError(
"FindSqlInjection caught exception while analyzing " + classContext.getFullyQualifiedMethodName(method),
e);
} catch (RuntimeException e) {
bugReporter.logError(
"FindSqlInjection caught exception while analyzing " + classContext.getFullyQualifiedMethodName(method),
e);
}
}
}
private boolean isStringAppend(Instruction ins, ConstantPoolGen cpg) {
if (ins instanceof INVOKEVIRTUAL) {
INVOKEVIRTUAL invoke = (INVOKEVIRTUAL) ins;
if (invoke.getMethodName(cpg).equals("append") && invoke.getClassName(cpg).startsWith("java.lang.StringB")) {
String sig = invoke.getSignature(cpg);
char firstChar = sig.charAt(1);
return firstChar == '[' || firstChar == 'L';
}
}
return false;
}
private boolean isConstantStringLoad(Location location, ConstantPoolGen cpg) {
Instruction ins = location.getHandle().getInstruction();
if (ins instanceof LDC) {
LDC load = (LDC) ins;
Object value = load.getValue(cpg);
if (value instanceof String) {
return true;
}
}
return false;
}
static final Pattern openQuotePattern = Pattern.compile("((^')|[^\\p{Alnum}]')$");
public static boolean isOpenQuote(String s) {
return openQuotePattern.matcher(s).find();
}
static final Pattern closeQuotePattern = Pattern.compile("^'($|[^\\p{Alnum}])");
public static boolean isCloseQuote(String s) {
return closeQuotePattern.matcher(s).find();
}
private StringAppendState updateStringAppendState(Location location, ConstantPoolGen cpg, StringAppendState stringAppendState)
{
InstructionHandle handle = location.getHandle();
Instruction ins = handle.getInstruction();
if (!isConstantStringLoad(location, cpg)) {
throw new IllegalArgumentException("instruction must be LDC");
}
LDC load = (LDC) ins;
Object value = load.getValue(cpg);
String stringValue = ((String) value).trim();
if (stringValue.startsWith(",") || stringValue.endsWith(",")) {
stringAppendState.setSawComma(handle);
}
if (isCloseQuote(stringValue) && stringAppendState.getSawOpenQuote(handle)) {
stringAppendState.setSawCloseQuote(handle);
}
if (isOpenQuote(stringValue)) {
stringAppendState.setSawOpenQuote(handle);
}
return stringAppendState;
}
private boolean isPreparedStatementDatabaseSink(Instruction ins, ConstantPoolGen cpg) {
if (!(ins instanceof INVOKEINTERFACE)) {
return false;
}
INVOKEINTERFACE invoke = (INVOKEINTERFACE) ins;
String methodName = invoke.getMethodName(cpg);
String methodSignature = invoke.getSignature(cpg);
String interfaceName = invoke.getClassName(cpg);
if (methodName.equals("prepareStatement") && interfaceName.equals("java.sql.Connection")
&& methodSignature.startsWith("(Ljava/lang/String;")) {
return true;
}
return false;
}
private boolean isExecuteDatabaseSink(InvokeInstruction ins, ConstantPoolGen cpg) {
if (!(ins instanceof INVOKEINTERFACE)) {
return false;
}
INVOKEINTERFACE invoke = (INVOKEINTERFACE) ins;
String methodName = invoke.getMethodName(cpg);
String methodSignature = invoke.getSignature(cpg);
String interfaceName = invoke.getClassName(cpg);
if (methodName.startsWith("execute") && interfaceName.equals("java.sql.Statement")
&& methodSignature.startsWith("(Ljava/lang/String;")) {
return true;
}
return false;
}
private boolean isDatabaseSink(InvokeInstruction ins, ConstantPoolGen cpg) {
return isPreparedStatementDatabaseSink(ins, cpg) || isExecuteDatabaseSink(ins, cpg);
}
private StringAppendState getStringAppendState(CFG cfg, ConstantPoolGen cpg) throws CFGBuilderException {
StringAppendState stringAppendState = new StringAppendState();
String sig = method.getSignature();
sig = sig.substring(0, sig.indexOf(')'));
if (sig.indexOf("java/lang/String") >= 0) {
stringAppendState.setSawInitialTaint();
}
for (Iterator<Location> i = cfg.locationIterator(); i.hasNext();) {
Location location = i.next();
InstructionHandle handle = location.getHandle();
Instruction ins = handle.getInstruction();
if (isConstantStringLoad(location, cpg)) {
stringAppendState = updateStringAppendState(location, cpg, stringAppendState);
} else if (isStringAppend(ins, cpg)) {
stringAppendState.setSawAppend(handle);
Location prevLocation = getPreviousLocation(cfg, location, true);
if (prevLocation != null && !isSafeValue(prevLocation, cpg)) {
stringAppendState.setSawUnsafeAppend(handle);
}
} else if (ins instanceof InvokeInstruction) {
InvokeInstruction inv = (InvokeInstruction) ins;
String sig1 = inv.getSignature(cpg);
String sig2 = sig1.substring(sig1.indexOf(')'));
if (sig2.indexOf("java/lang/String") >= 0) {
String methodName = inv.getMethodName(cpg);
String className = inv.getClassName(cpg);
if (methodName.equals("valueOf") && className.equals("java.lang.String")
&& sig1.equals("(Ljava/lang/Object;)Ljava/lang/String;")) {
try {
TypeDataflow typeDataflow = classContext.getTypeDataflow(method);
TypeFrame frame = typeDataflow.getFactAtLocation(location);
if (!frame.isValid()) {
// This basic block is probably dead
continue;
}
Type operandType = frame.getTopValue();
if (operandType.equals(TopType.instance())) {
// unreachable
continue;
}
String sig3 = operandType.getSignature();
if (!sig3.equals("Ljava/lang/String;")) {
stringAppendState.setSawTaint(handle);
}
} catch (CheckedAnalysisException e) {
stringAppendState.setSawTaint(handle);
}
} else if (className.startsWith("java.lang.String") || className.equals("java.lang.Long")
|| className.equals("java.lang.Integer") || className.equals("java.lang.Float")
|| className.equals("java.lang.Double") || className.equals("java.lang.Short")
|| className.equals("java.lang.Byte") || className.equals("java.lang.Character")) {
// ignore it
assert true;
} else if (methodName.startsWith("to") && methodName.endsWith("String") && methodName.length() > 8) {
// ignore it
assert true;
} else if (className.startsWith("javax.servlet") && methodName.startsWith("get")) {
stringAppendState.setSawTaint(handle);
stringAppendState.setSawSeriousTaint(handle);
} else {
stringAppendState.setSawTaint(handle);
}
}
} else if (ins instanceof GETFIELD) {
GETFIELD getfield = (GETFIELD) ins;
String sig2 = getfield.getSignature(cpg);
if (sig2.indexOf("java/lang/String") >= 0) {
stringAppendState.setSawTaint(handle);
}
}
}
return stringAppendState;
}
private boolean isSafeValue(Location location, ConstantPoolGen cpg) throws CFGBuilderException {
Instruction prevIns = location.getHandle().getInstruction();
if (prevIns instanceof LDC || prevIns instanceof GETSTATIC) {
return true;
}
if (prevIns instanceof InvokeInstruction) {
String methodName = ((InvokeInstruction) prevIns).getMethodName(cpg);
if (methodName.startsWith("to") && methodName.endsWith("String") && methodName.length() > 8) {
return true;
}
}
if (prevIns instanceof AALOAD) {
CFG cfg = classContext.getCFG(method);
Location prev = getPreviousLocation(cfg, location, true);
if (prev != null) {
Location prev2 = getPreviousLocation(cfg, prev, true);
if (prev2 != null && prev2.getHandle().getInstruction() instanceof GETSTATIC) {
GETSTATIC getStatic = (GETSTATIC) prev2.getHandle().getInstruction();
if (getStatic.getSignature(cpg).equals("[Ljava/lang/String;")) {
return true;
}
}
}
}
return false;
}
private @CheckForNull
InstructionHandle getPreviousInstruction(InstructionHandle handle, boolean skipNops) {
while (handle.getPrev() != null) {
handle = handle.getPrev();
Instruction prevIns = handle.getInstruction();
if (!(prevIns instanceof NOP && skipNops)) {
return handle;
}
}
return null;
}
private @CheckForNull
Location getPreviousLocation(CFG cfg, Location startLocation, boolean skipNops) {
Location loc = startLocation;
InstructionHandle prev = getPreviousInstruction(loc.getHandle(), skipNops);
if (prev != null) {
return new Location(prev, loc.getBasicBlock());
}
BasicBlock block = loc.getBasicBlock();
while (true) {
block = cfg.getPredecessorWithEdgeType(block, EdgeTypes.FALL_THROUGH_EDGE);
if (block == null) {
return null;
}
InstructionHandle lastInstruction = block.getLastInstruction();
if (lastInstruction != null) {
return new Location(lastInstruction, block);
}
}
}
private BugInstance generateBugInstance(JavaClass javaClass, MethodGen methodGen, InstructionHandle handle,
StringAppendState stringAppendState) {
Instruction instruction = handle.getInstruction();
ConstantPoolGen cpg = methodGen.getConstantPool();
int priority = LOW_PRIORITY;
boolean sawSeriousTaint = false;
if (stringAppendState.getSawAppend(handle)) {
if (stringAppendState.getSawOpenQuote(handle) && stringAppendState.getSawCloseQuote(handle)) {
priority = HIGH_PRIORITY;
} else if (stringAppendState.getSawComma(handle)) {
priority = NORMAL_PRIORITY;
}
if (!stringAppendState.getSawUnsafeAppend(handle)) {
priority += 2;
} else if (stringAppendState.getSawSeriousTaint(handle)) {
priority--;
sawSeriousTaint = true;
} else if (!stringAppendState.getSawTaint(handle)) {
priority++;
}
}
String description = "TESTING";
if (instruction instanceof InvokeInstruction && isExecuteDatabaseSink((InvokeInstruction) instruction, cpg)) {
description = "SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE";
} else if (isPreparedStatementDatabaseSink(instruction, cpg)) {
description = "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING";
}
BugInstance bug = new BugInstance(this, description, priority);
bug.addClassAndMethod(methodGen, javaClass.getSourceFileName());
if (description.equals("TESTING")) {
bug.addString("Incomplete report invoking non-constant SQL string");
}
if (sawSeriousTaint) {
bug.addString("non-constant SQL string involving HTTP taint");
}
return bug;
}
Method method;
ClassContext classContext;
private void analyzeMethod(ClassContext classContext, Method method) throws DataflowAnalysisException, CFGBuilderException {
JavaClass javaClass = classContext.getJavaClass();
this.method = method;
this.classContext = classContext;
MethodGen methodGen = classContext.getMethodGen(method);
if (methodGen == null) {
return;
}
ConstantPoolGen cpg = methodGen.getConstantPool();
CFG cfg = classContext.getCFG(method);
StringAppendState stringAppendState = getStringAppendState(cfg, cpg);
ConstantDataflow dataflow = classContext.getConstantDataflow(method);
for (Iterator<Location> i = cfg.locationIterator(); i.hasNext();) {
Location location = i.next();
Instruction ins = location.getHandle().getInstruction();
if (!(ins instanceof InvokeInstruction)) {
continue;
}
InvokeInstruction invoke = (InvokeInstruction) ins;
if (isDatabaseSink(invoke, cpg)) {
ConstantFrame frame = dataflow.getFactAtLocation(location);
int numArguments = frame.getNumArguments(invoke, cpg);
Constant value = frame.getStackValue(numArguments - 1);
if (!value.isConstantString()) {
// TODO: verify it's the same string represented by
// stringAppendState
// FIXME: will false positive on const/static strings
// returns by methods
Location prev = getPreviousLocation(cfg, location, true);
if (prev == null || !isSafeValue(prev, cpg)) {
BugInstance bug = generateBugInstance(javaClass, methodGen, location.getHandle(), stringAppendState);
if(!testingEnabled && "TESTING".equals(bug.getType())){
continue;
}
bugAccumulator.accumulateBug(
bug,
SourceLineAnnotation.fromVisitedInstruction(classContext, methodGen,
javaClass.getSourceFileName(), location.getHandle()));
}
}
}
}
bugAccumulator.reportAccumulatedBugs();
}
@Override
public void report() {
}
}