SOLR-12891 MacroExpander will no longer will expand URL parameters by

default inside of the 'expr' parameter, add InjectionDefense class
for safer handling of untrusted data in streaming expressions and add
-DStreamingExpressionMacros system property to revert to legacy behavior

(cherry picked from commit 9edc557f4526ffbbf35daea06972eb2c595e692b)
This commit is contained in:
Gus Heck 2019-03-12 10:46:30 -04:00
parent 8d0652451e
commit 470813143d
12 changed files with 625 additions and 140 deletions

View File

@ -30,10 +30,15 @@ Jetty 9.4.14.v20181114
Upgrade Notes
----------------------
When requesting the status of an async request via REQUESTSTATUS collections API, the response will
include the list of internal async requests (if any) in the "success" or "failed" keys (in addition
to them being included outside those keys for backwards compatibility). See SOLR-12708 for more
details
* When requesting the status of an async request via REQUESTSTATUS collections API, the response will
include the list of internal async requests (if any) in the "success" or "failed" keys (in addition
to them being included outside those keys for backwards compatibility). See SOLR-12708 for more
details
* SOLR-12891: MacroExpander will no longer will expand URL parameters inside of the 'expr' parameter (used by streaming
expressions) Additionally, users are advised to use the 'InjectionDefense' class when constructing streaming
expressions that include user supplied data to avoid risks similar to SQL injection. The legacy behavior of
expanding the 'expr' parameter can be reinstated with -DStreamingExpressionMacros=true passed to the JVM at startup.
New Features
----------------------

View File

@ -36,7 +36,6 @@ public class MacroExpander {
private int level;
private final boolean failOnMissingParams;
public MacroExpander(Map<String,String[]> orig) {
this(orig, false);
}
@ -58,8 +57,12 @@ public class MacroExpander {
boolean changed = false;
for (Map.Entry<String,String[]> entry : orig.entrySet()) {
String k = entry.getKey();
String newK = expand(k);
String[] values = entry.getValue();
if (!isExpandingExpr() && "expr".equals(k) ) { // SOLR-12891
expanded.put(k,values);
continue;
}
String newK = expand(k);
List<String> newValues = null;
for (String v : values) {
String newV = expand(v);
@ -92,6 +95,10 @@ public class MacroExpander {
return changed;
}
private Boolean isExpandingExpr() {
return Boolean.valueOf(System.getProperty("StreamingExpressionMacros", "false"));
}
public String expand(String val) {
level++;
try {

View File

@ -118,6 +118,7 @@ public class TestMacroExpander extends SolrTestCase {
public void testMap() { // see SOLR-9740, the second fq param was being dropped.
final Map<String,String[]> request = new HashMap<>();
request.put("fq", new String[] {"zero", "${one_ref}", "two", "${three_ref}"});
request.put("expr", new String[] {"${one_ref}"}); // expr is for streaming expressions, no replacement by default
request.put("one_ref",new String[] {"one"});
request.put("three_ref",new String[] {"three"});
Map expanded = MacroExpander.expand(request);
@ -125,6 +126,30 @@ public class TestMacroExpander extends SolrTestCase {
assertEquals("one", ((String[])expanded.get("fq"))[1]);
assertEquals("two", ((String[]) expanded.get("fq"))[2]);
assertEquals("three", ((String[]) expanded.get("fq"))[3]);
assertEquals("${one_ref}", ((String[])expanded.get("expr"))[0]);
}
@Test
public void testMapExprExpandOn() {
final Map<String,String[]> request = new HashMap<>();
request.put("fq", new String[] {"zero", "${one_ref}", "two", "${three_ref}"});
request.put("expr", new String[] {"${one_ref}"}); // expr is for streaming expressions, no replacement by default
request.put("one_ref",new String[] {"one"});
request.put("three_ref",new String[] {"three"});
// I believe that so long as this is sure to be reset before the end of the test we should
// be fine with respect to other tests.
String oldVal = System.getProperty("StreamingExpressionMacros","false");
System.setProperty("StreamingExpressionMacros", "true");
try {
Map expanded = MacroExpander.expand(request);
assertEquals("zero", ((String[])expanded.get("fq"))[0]);
assertEquals("one", ((String[])expanded.get("fq"))[1]);
assertEquals("two", ((String[]) expanded.get("fq"))[2]);
assertEquals("three", ((String[]) expanded.get("fq"))[3]);
assertEquals("one", ((String[])expanded.get("expr"))[0]);
} finally {
System.setProperty("StreamingExpressionMacros", oldVal);
}
}
}

View File

@ -99,11 +99,17 @@ The {solr-javadocs}/solr-solrj/org/apache/solr/client/solrj/io/package-summary.h
[source,java]
----
StreamFactory streamFactory = new DefaultStreamFactory().withCollectionZkHost("collection1", zkServer.getZkAddress());
ParallelStream pstream = (ParallelStream)streamFactory.constructStream("parallel(collection1, group(search(collection1, q=\"*:*\", fl=\"id,a_s,a_i,a_f\", sort=\"a_s asc,a_f asc\", partitionKeys=\"a_s\"), by=\"a_s asc\"), workers=\"2\", zkHost=\""+zkHost+"\", sort=\"a_s asc\")");
StreamFactory streamFactory = new DefaultStreamFactory().withCollectionZkHost("collection1", zkServer.getZkAddress());
InjectionDefense defense = new InjectionDefense("parallel(collection1, group(search(collection1, q=\"*:*\", fl=\"id,a_s,a_i,a_f\", sort=\"a_s asc,a_f asc\", partitionKeys=\"a_s\"), by=\"a_s asc\"), workers=\"2\", zkHost=\"?$?\", sort=\"a_s asc\")");
defense.addParameter(zkhost);
ParallelStream pstream = (ParallelStream)streamFactory.constructStream(defense.safeExpressionString());
----
Note that InjectionDefense need only be used if the string being inserted could contain user supplied data. See the
javadoc for `InjectionDefense` for usage details and SOLR-12891 for an example of the potential risks.
Also note that for security reasons normal parameter substitution no longer applies to the expr parameter
unless the jvm has been started with `-DStreamingExpressionMacros=true` (usually via `solr.in.sh`)
=== Data Requirements
Because streaming expressions relies on the `/export` handler, many of the field and field type requirements to use `/export` are also requirements for `/stream`, particularly for `sort` and `fl` parameters. Please see the section <<exporting-result-sets.adoc#exporting-result-sets,Exporting Result Sets>> for details.

View File

@ -94,7 +94,7 @@ public class Lang {
.withFunctionName("plist", ParallelListStream.class)
.withFunctionName("zplot", ZplotStream.class)
.withFunctionName("hashRollup", HashRollupStream.class)
.withFunctionName("noop", NoOpStream.class)
// metrics
.withFunctionName("min", MinMetric.class)

View File

@ -0,0 +1,107 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.client.solrj.io.stream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.apache.solr.client.solrj.io.Tuple;
import org.apache.solr.client.solrj.io.comp.StreamComparator;
import org.apache.solr.client.solrj.io.stream.expr.Explanation;
import org.apache.solr.client.solrj.io.stream.expr.Explanation.ExpressionType;
import org.apache.solr.client.solrj.io.stream.expr.Expressible;
import org.apache.solr.client.solrj.io.stream.expr.StreamExplanation;
import org.apache.solr.client.solrj.io.stream.expr.StreamExpression;
import org.apache.solr.client.solrj.io.stream.expr.StreamFactory;
/**
* A simple no-operation stream. Immediately returns eof. Mostly intended for use as
* a place holder in {@link org.apache.solr.client.solrj.io.stream.expr.InjectionDefense}.
*
* @since 8.0.0
*/
public class NoOpStream extends TupleStream implements Expressible {
private static final long serialVersionUID = 1;
private boolean finished;
public NoOpStream() throws IOException {
}
public NoOpStream(StreamExpression expression, StreamFactory factory) throws IOException {
}
@Override
public StreamExpression toExpression(StreamFactory factory) throws IOException{
return toExpression(factory, true);
}
private StreamExpression toExpression(StreamFactory factory, boolean includeStreams) throws IOException {
// function name
StreamExpression expression = new StreamExpression(factory.getFunctionName(this.getClass()));
return expression;
}
@Override
public Explanation toExplanation(StreamFactory factory) throws IOException {
return new StreamExplanation(getStreamNodeId().toString())
.withFunctionName(factory.getFunctionName(this.getClass()))
.withImplementingClass(this.getClass().getName())
.withExpressionType(ExpressionType.STREAM_DECORATOR)
.withExpression(toExpression(factory, false).toString());
}
public void setStreamContext(StreamContext context) {
}
public List<TupleStream> children() {
List<TupleStream> l = new ArrayList<TupleStream>();
return l;
}
public void open() throws IOException {
}
public void close() throws IOException {
}
public Tuple read() throws IOException {
HashMap m = new HashMap();
m.put("EOF", true);
Tuple tuple = new Tuple(m);
return tuple;
}
/** Return the stream sort - ie, the order in which records are returned */
public StreamComparator getStreamSort(){
return null;
}
public int getCost() {
return 0;
}
}

View File

@ -0,0 +1,24 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.client.solrj.io.stream.expr;
class InjectedExpressionException extends IllegalStateException {
InjectedExpressionException(String s) {
super(s);
}
}

View File

@ -0,0 +1,199 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.client.solrj.io.stream.expr;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A class with which to safely build a streaming expression. Three types of parameters
* (String, Numeric, Expression) are accepted and minimally type checked. All parameters
* are positional (unnamed) so the order in which parameters are added must correspond to
* the order of the parameters in the supplied expression string.<br><br>
*
* <p>Specifically, this class verifies that the parameter substitutions do not inject
* additional expressions, and that the parameters are strings, valid numbers or valid
* expressions producing the expected number of sub-expressions. The idea is not to provide
* full type safety but rather to heuristically prevent the injection of malicious
* expressions. The template expression and the parameters supplied must not contain
* comments since injection of comments could be used to hide one or more of the expected
* expressions. Use {@link #stripComments(String)} to remove comments.<br><br>
*
* <p>Valid patterns for parameters are:
* <ul>
* <li>?$? for strings</li>
* <li>?#? for numeric parameters in integer or decimal format (no exponents)</li>
* <li>?(n)? for expressions producing n sub-expressions (minimum n=1)</li>
* </ul>
*
* @since 8.0.0
*/
public class InjectionDefense {
private static final Pattern STRING_PARAM = Pattern.compile("\\?\\$\\?");
private static final Pattern NUMBER_PARAM = Pattern.compile("\\?#\\?");
private static final Pattern EXPRESSION_PARAM = Pattern.compile("\\?\\(\\d+\\)\\?");
private static final Pattern EXPRESSION_COUNT = Pattern.compile("\\d+");
private static final Pattern ANY_PARAM = Pattern.compile("\\?(?:[$#]|(?:\\(\\d+\\)))\\?");
private static final Pattern INT_OR_FLOAT = Pattern.compile("-?\\d+(?:\\.\\d+)?");
private String exprString;
private int expressionCount;
private List<String> params = new ArrayList<>();
@SuppressWarnings("WeakerAccess")
public InjectionDefense(String exprString) {
this.exprString = exprString;
checkExpression(exprString);
}
@SuppressWarnings("WeakerAccess")
public static String stripComments(String exprString) {
return StreamExpressionParser.stripComments(exprString);
}
public void addParameter(String param) {
params.add(param);
}
/**
* Provides an expression that is guaranteed to have the expected number of sub-expressions
*
* @return An expression object that should be safe from injection of additional expressions
*/
@SuppressWarnings("WeakerAccess")
public StreamExpression safeExpression() {
String exprStr = buildExpression();
System.out.println(exprStr);
StreamExpression parsed = StreamExpressionParser.parse(exprStr);
int actual = countExpressions(parsed);
if (actual != expressionCount) {
throw new InjectedExpressionException("Expected Expression count ("+expressionCount+") does not match actual final " +
"expression count ("+actual+")! (possible injection attack?)");
} else {
return parsed;
}
}
/**
* Provides a string that is guaranteed to parse to a legal expression and to have the expected
* number of sub-expressions.
*
* @return A string that should be safe from injection of additional expressions.
*/
@SuppressWarnings("WeakerAccess")
public String safeExpressionString() {
String exprStr = buildExpression();
StreamExpression parsed = StreamExpressionParser.parse(exprStr);
if (countExpressions(parsed) != expressionCount) {
throw new InjectedExpressionException("Expected Expression count does not match Actual final " +
"expression count! (possible injection attack?)");
} else {
return exprStr;
}
}
String buildExpression() {
Matcher anyParam = ANY_PARAM.matcher(exprString);
StringBuffer buff = new StringBuffer();
int pIdx = 0;
while (anyParam.find()) {
String found = anyParam.group();
String p = params.get(pIdx++);
if (found.contains("#")) {
if (!INT_OR_FLOAT.matcher(p).matches()) {
throw new NumberFormatException("Argument " + pIdx + " (" + p + ") is not numeric!");
}
}
anyParam.appendReplacement(buff, p);
}
anyParam.appendTail(buff);
// strip comments may add '\n' at the end so trim()
String result = buff.toString().trim();
String noComments = stripComments(result).trim();
if (!result.equals(noComments)) {
throw new IllegalStateException("Comments are not allowed in prepared expressions for security reasons " +
"please pre-process stripComments() first. If there were no comments, then they have been injected by " +
"a parameter value.");
}
return buff.toString().trim();
}
/**
* Perform some initial checks and establish the expected number of expressions
*
* @param exprString the expression to check.
*/
private void checkExpression(String exprString) {
exprString = STRING_PARAM.matcher(exprString).replaceAll("foo");
exprString = NUMBER_PARAM.matcher(exprString).replaceAll("0");
Matcher eMatcher = EXPRESSION_PARAM.matcher(exprString);
StringBuffer temp = new StringBuffer();
while (eMatcher.find()) {
Matcher counter = EXPRESSION_COUNT.matcher(eMatcher.group());
eMatcher.appendReplacement(temp, "noop()");
if (counter.find()) {
Integer subExprCount = Integer.valueOf(counter.group());
if (subExprCount < 1) {
throw new IllegalStateException("Expression Param must contribute at least 1 expression!" +
" ?(1)? is the minimum allowed ");
}
expressionCount += (subExprCount - 1); // the noop() we insert will get counted later.
}
}
eMatcher.appendTail(temp);
exprString = temp.toString();
StreamExpression parsed = StreamExpressionParser.parse(exprString);
if (parsed != null) {
expressionCount += countExpressions(parsed);
} else {
throw new IllegalStateException("Invalid expression (parse returned null):" + exprString);
}
}
private int countExpressions(StreamExpression expression) {
int result = 0;
List<StreamExpressionParameter> exprToCheck = new ArrayList<>();
exprToCheck.add(expression);
while (exprToCheck.size() > 0) {
StreamExpressionParameter remove = exprToCheck.remove(0);
if (remove instanceof StreamExpressionNamedParameter) {
remove = ((StreamExpressionNamedParameter) remove).getParameter();
}
if (remove instanceof StreamExpression) {
result++;
for (StreamExpressionParameter parameter : ((StreamExpression) remove).getParameters()) {
if (parameter instanceof StreamExpressionNamedParameter) {
parameter = ((StreamExpressionNamedParameter) parameter).getParameter();
}
if (parameter instanceof StreamExpression) {
exprToCheck.add(parameter);
}
}
}
}
return result;
}
}

View File

@ -46,26 +46,18 @@ public class StreamExpressionParser {
}
private static String stripComments(String clause) throws RuntimeException {
static String stripComments(String clause) throws RuntimeException {
StringBuilder builder = new StringBuilder();
BufferedReader reader = null;
try {
reader = new BufferedReader(new StringReader(clause));
String line = null;
try (BufferedReader reader = new BufferedReader(new StringReader(clause))) {
String line;
while ((line = reader.readLine()) != null) {
if(line.trim().startsWith("#")) {
continue;
} else {
builder.append(line+'\n');
if (!line.trim().startsWith("#")) {
builder.append(line).append('\n');
}
}
}catch(Exception e) {
} catch (Exception e) {
throw new RuntimeException(e);
} finally{
try {
reader.close();
} catch (Exception e) {}
}
return builder.toString();

View File

@ -75,7 +75,7 @@ public class TestLang extends SolrTestCase {
"convexHull", "getVertices", "getBaryCenter", "getArea", "getBoundarySize","oscillate",
"getAmplitude", "getPhase", "getAngularFrequency", "enclosingDisk", "getCenter", "getRadius",
"getSupportPoints", "pairSort", "log10", "plist", "recip", "pivot", "ltrim", "rtrim", "export",
"zplot", "natural", "repeat", "movingMAD", "hashRollup"};
"zplot", "natural", "repeat", "movingMAD", "hashRollup", "noop"};
@Test
public void testLang() {

View File

@ -412,7 +412,9 @@ public class StreamExpressionTest extends SolrCloudTestCase {
@Test
public void testParameterSubstitution() throws Exception {
String oldVal = System.getProperty("StreamingExpressionMacros", "false");
System.setProperty("StreamingExpressionMacros", "true");
try {
new UpdateRequest()
.add(id, "0", "a_s", "hello0", "a_i", "0", "a_f", "0")
.add(id, "2", "a_s", "hello2", "a_i", "2", "a_f", "0")
@ -439,7 +441,7 @@ public class StreamExpressionTest extends SolrCloudTestCase {
tuples = getTuples(stream);
assertEquals(4, tuples.size());
assertOrder(tuples, 0,1,3,4);
assertOrder(tuples, 0, 1, 3, 4);
// Basic test desc
sParams.set("mySort", "a_f desc");
@ -457,6 +459,9 @@ public class StreamExpressionTest extends SolrCloudTestCase {
assertEquals(5, tuples.size());
assertOrder(tuples, 0, 2, 1, 3, 4);
} finally {
System.setProperty("StreamingExpressionMacros", oldVal);
}
}

View File

@ -0,0 +1,115 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.client.solrj.io.stream.expr;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class InjectionDefenseTest {
private static final String EXPLOITABLE = "let(a=search(foo,q=\"time_dt:[?$? TO ?$?]\",fl=\"id,time_dt\",sort=\"time_dt asc\"))";
private static final String NUMBER = "let(a=search(foo,q=\"gallons_f:[?#? TO ?#?]\",fl=\"id,gallons_f,time_dt\",sort=\"time_dt asc\"))";
private static final String NUMBER_OK = "let(a=search(foo,q=\"gallons_f:[2 TO 3.5]\",fl=\"id,gallons_f,time_dt\",sort=\"time_dt asc\"))";
private static final String ALLOWED = "let(a=search(foo,q=\"time_dt:[?$? TO ?$?]\",fl=\"id,time_dt\",sort=\"time_dt asc\"), x=?(2)?)";
private static final String INJECTED = "let(a=search(foo,q=\"time_dt:[2000-01-01T00:00:00Z TO 2020-01-01T00:00:00Z]\",fl=\"id,time_dt\",sort=\"time_dt asc\"), x=jdbc( connection=\"jdbc:postgresql://localhost:5432/ouchdb\",sql=\"select * from users\",sort=\"id asc\"),z=jdbc( connection=\"jdbc:postgresql://localhost:5432/ouchdb\",sql=\"select * from race_cars\",sort=\"id asc\"))";
@Test(expected = InjectedExpressionException.class)
public void testSafeExpression() {
InjectionDefense defender = new InjectionDefense(EXPLOITABLE);
defender.addParameter("2000-01-01T00:00:00Z");
defender.addParameter("2020-01-01T00:00:00Z]\",fl=\"id\",sort=\"id asc\"), b=jdbc( connection=\"jdbc:postgresql://localhost:5432/ouchdb\",sql=\"select * from users\",sort=\"id asc\"),c=search(foo,q=\"time_dt:[* TO 2020-01-01T00:00:00Z");
defender.safeExpression();
}
@Test(expected = InjectedExpressionException.class)
public void testSafeString() {
InjectionDefense defender = new InjectionDefense(EXPLOITABLE);
defender.addParameter("2000-01-01T00:00:00Z");
defender.addParameter("2020-01-01T00:00:00Z]\",fl=\"id\",sort=\"id asc\"), b=jdbc( connection=\"jdbc:postgresql://localhost:5432/ouchdb\",sql=\"select * from users\",sort=\"id asc\"),c=search(foo,q=\"time_dt:[* TO 2020-01-01T00:00:00Z");
defender.safeExpressionString();
}
@Test
public void testExpectedInjectionOfExpressions() {
InjectionDefense defender = new InjectionDefense(ALLOWED);
defender.addParameter("2000-01-01T00:00:00Z");
defender.addParameter("2020-01-01T00:00:00Z");
defender.addParameter("jdbc( connection=\"jdbc:postgresql://localhost:5432/ouchdb\",sql=\"select * from users\",sort=\"id asc\"),z=jdbc( connection=\"jdbc:postgresql://localhost:5432/ouchdb\",sql=\"select * from race_cars\",sort=\"id asc\")");
// no exceptions
assertNotNull(defender.safeExpression());
assertEquals(INJECTED, defender.safeExpressionString());
}
@Test(expected = InjectedExpressionException.class)
public void testWrongNumberInjected() {
InjectionDefense defender = new InjectionDefense(ALLOWED);
defender.addParameter("2000-01-01T00:00:00Z");
defender.addParameter("2020-01-01T00:00:00Z");
defender.addParameter("jdbc( connection=\"jdbc:postgresql://localhost:5432/ouchdb\",sql=\"select * from users\",sort=\"id asc\")");
// no exceptions
defender.safeExpression();
assertEquals(INJECTED, defender.safeExpressionString());
}
@Test
public void testBuildExpression() {
InjectionDefense defender = new InjectionDefense(ALLOWED);
defender.addParameter("2000-01-01T00:00:00Z");
defender.addParameter("2020-01-01T00:00:00Z");
defender.addParameter("jdbc( connection=\"jdbc:postgresql://localhost:5432/ouchdb\",sql=\"select * from users\",sort=\"id asc\"),z=jdbc( connection=\"jdbc:postgresql://localhost:5432/ouchdb\",sql=\"select * from race_cars\",sort=\"id asc\")");
assertEquals(INJECTED, defender.buildExpression());
}
@Test
public void testInjectNumber() {
InjectionDefense defender = new InjectionDefense(NUMBER);
defender.addParameter("2");
defender.addParameter("3.5");
assertEquals(NUMBER_OK, defender.buildExpression());
}
@Test(expected = NumberFormatException.class)
public void testInjectAlphaFail() {
InjectionDefense defender = new InjectionDefense(NUMBER);
defender.addParameter("a");
defender.addParameter("3.5");
defender.buildExpression();
}
}