/** * 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.taintanalysis; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.regex.Pattern; /** * Summary of information about a method related to taint analysis.<br /> *<br /> * For loading sinks files please see {@link com.h3xstream.findsecbugs.injection.SinksLoader} * * @author David Formanek (Y Soft Corporation, a.s.) */ public class TaintMethodConfig implements TaintTypeConfig { private Taint outputTaint = null; private final Set<Integer> mutableStackIndices; private final boolean isConfigured; public static final TaintMethodConfig SAFE_CONFIG; protected static final Pattern fullMethodPattern; protected static final Pattern configPattern; static { SAFE_CONFIG = new TaintMethodConfig(false); SAFE_CONFIG.outputTaint = new Taint(Taint.State.SAFE); String javaIdentifierRegex = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"; String classWithPackageRegex = javaIdentifierRegex+"(\\/"+javaIdentifierRegex+")*"; String typeRegex = "(\\[)*((L" + classWithPackageRegex + ";)|B|C|D|F|I|J|S|Z)"; String returnRegex = "(V|(" + typeRegex + "))"; String methodRegex = "(("+javaIdentifierRegex+"(\\$extension)?)|(<init>))"; String signatureRegex = "\\((" + typeRegex + ")*\\)" + returnRegex; String fullMathodNameRegex = classWithPackageRegex + "\\." + methodRegex + signatureRegex; fullMethodPattern = Pattern.compile(fullMathodNameRegex); String taintStateNameRegex = "[A-Z_]+"; String stackIndexRegex = "[0-9]+"; String resultStateOrStackIndexRegex = "("+ taintStateNameRegex + "|" + stackIndexRegex + ")"; String commaSeparatedTaintResultsRegex = "("+resultStateOrStackIndexRegex+",)*"+resultStateOrStackIndexRegex; String taintTagNameRegex = taintStateNameRegex; String taintTagModificationRegex = "[+-]" + taintTagNameRegex; String commaSeparatedTaintTagModificationRegex = "("+taintTagModificationRegex+",)*"+taintTagModificationRegex; String commaSeparatedStackMutationIndexesRegex = "("+stackIndexRegex+",)*" + stackIndexRegex; String configRegex = commaSeparatedTaintResultsRegex; configRegex += "(\\|" + commaSeparatedTaintTagModificationRegex + ")?"; configRegex += "(#" + commaSeparatedStackMutationIndexesRegex+ ")?"; configPattern = Pattern.compile(configRegex); } /** * Constructs an empty summary * * @param isConfigured true for configured summaries, false for derived */ public TaintMethodConfig(boolean isConfigured) { outputTaint = null; mutableStackIndices = new HashSet<Integer>(); this.isConfigured = isConfigured; } /** * Creates a copy of the summary (output taint not copied) * * @param config Original taint config to copy */ public TaintMethodConfig(TaintMethodConfig config) { this.mutableStackIndices = config.mutableStackIndices; this.isConfigured = config.isConfigured; } /** * Returns all stack indices modified by method if there are any * * @return unmodifiable collection of indices * @throws IllegalStateException if there are not indices set */ public Collection<Integer> getMutableStackIndices() { if (!hasMutableStackIndices()) { throw new IllegalStateException("stack indices not set"); } return Collections.unmodifiableCollection(mutableStackIndices); } /** * Checks if there are any indices modified by method * * @return true if some index is set, false otherwise */ public boolean hasMutableStackIndices() { assert mutableStackIndices != null; return !mutableStackIndices.isEmpty(); } /** * Adds a stack index modified by method * * @param mutableStackIndex index to add * @throws IllegalArgumentException if index is negative */ public void addMutableStackIndex(int mutableStackIndex) { if (mutableStackIndex < 0) { throw new IllegalArgumentException("negative index"); } mutableStackIndices.add(mutableStackIndex); } /** * Returns the output taint of the method describing the taint transfer * * @return a copy of the output taint or null if not set */ public Taint getOutputTaint() { if (outputTaint == null) { return null; } return new Taint(outputTaint); } /** * Sets the output taint of the method describing the taint transfer, * copy of the parameter is made and variable index is invalidated * * @param taint output taint to set */ public void setOuputTaint(Taint taint) { if (taint == null) { this.outputTaint = null; return; } Taint taintCopy = new Taint(taint); taintCopy.invalidateVariableIndex(); this.outputTaint = taintCopy; } /** * Constructs a default constructor summary * (modifies 2 stack items with UNKNOWN taint state) * * @param stackSize size of the parameter stack (including instance) * @return new instance of default summary * @throws IllegalArgumentException for stackSize < 1 */ public static TaintMethodConfig getDefaultConstructorConfig(int stackSize) { if (stackSize < 1) { throw new IllegalArgumentException("stack size less than 1"); } TaintMethodConfig config = new TaintMethodConfig(false); config.outputTaint = new Taint(Taint.State.UNKNOWN); config.mutableStackIndices.add(stackSize - 1); config.mutableStackIndices.add(stackSize); return config; } /** * Checks if the summary needs to be saved or has no information value * * @return true if summary should be saved, false otherwise */ public boolean isInformative() { if (this == SAFE_CONFIG) { // these are loaded automatically, do not need to store them return false; } if (outputTaint == null) { return false; } if (!outputTaint.isUnknown()) { return true; } if (outputTaint.hasParameters()) { return true; } if (outputTaint.getRealInstanceClass() != null) { return true; } if (outputTaint.hasTags() || outputTaint.isRemovingTags()) { return true; } return false; } /** * Checks if the summary is configured or derived * * @return true if configured, false if derived */ public boolean isConfigured() { return isConfigured; } @Override public String toString() { if (outputTaint == null) { return ""; } StringBuilder sb = new StringBuilder(); if (outputTaint.isUnknown() && outputTaint.hasParameters()) { appendJoined(sb, outputTaint.getParameters()); Taint.State nonParametricState = outputTaint.getNonParametricState(); assert nonParametricState != null; if (nonParametricState != Taint.State.INVALID) { sb.append(",").append(nonParametricState.name()); } } else { sb.append(outputTaint.getState().name()); } if (outputTaint.hasTags()) { sb.append('|'); boolean isFirst = true; for (Taint.Tag tag : outputTaint.getTags()) { if (isFirst) { isFirst = false; } else { sb.append(','); } sb.append('+'); sb.append(tag.name()); } if (outputTaint.isRemovingTags()) { sb.append(','); } } if (outputTaint.isRemovingTags()) { sb.append('|'); boolean isFirst = true; for (Taint.Tag tag : outputTaint.getTagsToRemove()) { if (isFirst) { isFirst = false; } else { sb.append(','); } sb.append('-'); sb.append(tag.name()); } } if (hasMutableStackIndices()) { sb.append("#"); appendJoined(sb, mutableStackIndices); } String realInstanceClassName = outputTaint.getRealInstanceClassName(); if (realInstanceClassName != null) { sb.append(" (").append(realInstanceClassName).append(")"); } return sb.toString(); } private static void appendJoined(StringBuilder sb, Collection<Integer> objects) { assert sb != null && objects != null; int count = objects.size(); Integer[] array = objects.toArray(new Integer[count]); sb.append(array[0]); for (int i = 1; i < count; i++) { sb.append(",").append(array[i]); } } public static boolean accepts(String typeSignature, String config) { return fullMethodPattern.matcher(typeSignature).matches() && configPattern.matcher(config).matches(); } /** * Loads method summary from String. <br /> * <br /> * The summary should have the following syntax:<br /> * <code>resultTaintState |resultTaintTags #stackMutationIndexes</code>, where <ul> * <li><code>resultTaintState</code> are stack indexes or {@link Taint.State} enums separated by comma, e.g. <code>1,2</code> or <code>TAINTED</code></li> * <li><code>resultTaintTags</code> are {@link Taint.Tag} enums separated by comma, started with plus or minus sign, e.g. <code>+CR_ENCODED,-XSS_SAFE</code></li> * <li><code>stackMutationIndexes</code> are stack indexes separated by comma, e.g. <code>3,4</code></li> * </ul> * * Example: <br/> * <code>org/owasp/esapi/Encoder.encodeForHTML(Ljava/lang/String;)Ljava/lang/String;:0|+XSS_SAFE,+CR_ENCODED,+LF_ENCODED</code><br /> * <ul> * <li>Here the summary is: <code>0|+XSS_SAFE,+CR_ENCODED,+LF_ENCODED</code></li> * <li>The result taint will be merged with the first method argument, index 0</li> * <li>The result taint will have <code>XSS_SAFE</code>, <code>CR_ENCODED</code> and <code>CR_ENCODED</code> tags set</li> * <li>Practically, the result string will keep the taint but will receive XSS_SAFE tags which are processed by XssJspDetector</li> * </ul> * * Example: <br/> * <code>org/owasp/esapi/Encoder.decodeForHTML(Ljava/lang/String;)Ljava/lang/String;:0|-XSS_SAFE,-CR_ENCODED,-LF_ENCODED</code><br /> * <ul> * <li>Here the result taint will be merged with the first method argument, index 0</li> * <li>The framework removes <code>XSS_SAFE</code>, <code>CR_ENCODED</code> and <code>CR_ENCODED</code> tags</li> * <li>Practically, the result string will keep the taint but XSS_SAFE tag is removed again</li> * </ul> * * Example: <br/> * <code>java/lang/StringBuilder.<init>(Ljava/lang/String;)V:0#1,2</code> * <ul> * <li>Here the result taint will be merged with the first constructor argument, index 0</li> * <li>Framework also mutates taint of the StringBuilder object itself with the result taint, index 1</li> * <li>Because we are in a constructor, we need to specify one more taint index => 2</li> * <li>Practically, when the original String is tainted then StringBuilder will be tainted too</li> * </ul> * * Example: <br/> * <code>java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;:0,1#1</code> * <ul> * <li>Here the result taint will be merged with the method argument and the taint of the StringBuilder, index 0 and 1</li> * <li>Framework also mutates taint of the StringBuilder object itself with the result taint, index 1</li> * <li>Practically, the result taint is a merge of the String argument and previous taint of StringBuilder, on top propagates the result into StringBuilder's taint state again</li> * </ul> * Important notes about stack indexes:<ul> * <li>long and double types take two slots on stack and need two subsequent indexes, i.e. index of the String parameter in <code>method(Ljava/lang/String;D)</code> is 2, not 1 as one would expect</li> * <li>taint analysis adds two Taint objects on stack for constructors, don't forget to specify both</li> * </ul> * * @param taintConfig (state or parameter indices to merge separated by comma)#mutable position * @return initialized object with taint method summary * @throws java.io.IOException for bad format of parameter * @throws NullPointerException if argument is null */ @Override public TaintMethodConfig load(String taintConfig) throws IOException { if (taintConfig == null) { throw new NullPointerException("string is null"); } taintConfig = taintConfig.trim(); if (taintConfig.isEmpty()) { throw new IOException("No taint method config specified"); } taintConfig = loadMutableStackIndeces(taintConfig); String[] tuple = taintConfig.split("\\|"); if (tuple.length == 2) { taintConfig = tuple[0]; } else if (tuple.length != 1) { throw new IOException("Bad format: only one '|' expected"); } loadStatesAndParameters(taintConfig); if (tuple.length == 2) { loadTags(tuple[1]); } return this; } private String loadMutableStackIndeces(String str) throws IOException { String[] tuple = str.split("#"); if (tuple.length == 2) { str = tuple[0]; try { String[] indices = tuple[1].split(","); for (String index : indices) { addMutableStackIndex(Integer.parseInt(index.trim())); } } catch (NumberFormatException ex) { throw new IOException("Cannot parse mutable stack offsets", ex); } } else if (tuple.length != 1) { throw new IOException("Bad format: only one '#' expected"); } return str; } private void loadStatesAndParameters(String str) throws IOException { if (str.isEmpty()) { throw new IOException("No taint information set"); } else if (isTaintStateValue(str)) { setOuputTaint(Taint.valueOf(str)); } else { String[] tuple = str.split(","); int count = tuple.length; Taint taint = new Taint(Taint.State.UNKNOWN); for (int i = 0; i < count; i++) { String indexOrState = tuple[i].trim(); if (isTaintStateValue(indexOrState)) { taint.setNonParametricState(Taint.State.valueOf(indexOrState)); } else { try { taint.addParameter(Integer.parseInt(indexOrState)); } catch (NumberFormatException ex) { throw new IOException("Cannot parse parameter offset " + i, ex); } } } setOuputTaint(taint); } } private void loadTags(String tagInfo) throws IOException { if (tagInfo.isEmpty()) { throw new IOException("No taint tags specified"); } for (String tagName : tagInfo.split(",")) { char sign = tagName.charAt(0); tagName = tagName.substring(1); if (!isTaintTagValue(tagName)) { throw new IOException("Bad format: unknown taint tag " + tagName); } Taint.Tag tag = Taint.Tag.valueOf(tagName); if (outputTaint.hasTag(tag) || outputTaint.getTagsToRemove().contains(tag)) { throw new IOException("Bad format: tag " + tag + " already present"); } switch (sign) { case '+': outputTaint.addTag(tag); break; case '-': outputTaint.removeTag(tag); break; default: throw new IOException("Bad format: taint tag sign must be + or - but is " + sign); } } } private boolean isTaintTagValue(String value) { assert value != null && !value.isEmpty(); for (Taint.Tag tag : Taint.Tag.values()) { if (tag.name().equals(value)) { return true; } } return false; } private boolean isTaintStateValue(String value) { assert value != null && !value.isEmpty(); Taint.State[] states = Taint.State.values(); for (Taint.State state : states) { if (state.name().equals(value)) { return true; } } return false; } } /* @generated */