Skip to content
Permalink
Browse files Browse the repository at this point in the history
Merge pull request from GHSA-99c3-qc2q-p94m
  • Loading branch information
sikeoka committed Feb 10, 2023
1 parent 424f4f0 commit 64fb4c4
Show file tree
Hide file tree
Showing 17 changed files with 614 additions and 170 deletions.
Expand Up @@ -45,6 +45,7 @@
import org.geotools.filter.function.InFunction;
import org.geotools.filter.spatial.BBOXImpl;
import org.geotools.jdbc.EnumMapper;
import org.geotools.jdbc.EscapeSql;
import org.geotools.jdbc.JDBCDataStore;
import org.geotools.jdbc.JoinId;
import org.geotools.jdbc.JoinPropertyName;
Expand Down Expand Up @@ -230,6 +231,9 @@ public class FilterToSQL implements FilterVisitor, ExpressionVisitor {
/** Whether the encoder should try to encode "in" function into a SQL IN operator */
protected boolean inEncodingEnabled = true;

/** Whether to escape backslash characters in string literals */
protected boolean escapeBackslash = false;

/** Default constructor */
public FilterToSQL() {}

Expand Down Expand Up @@ -265,6 +269,16 @@ public void setInEncodingEnabled(boolean inEncodingEnabled) {
this.inEncodingEnabled = inEncodingEnabled;
}

/** @return whether to escape backslash characters in string literals */
public boolean isEscapeBackslash() {
return escapeBackslash;
}

/** @param escapeBackslash whether to escape backslash characters in string literals */
public void setEscapeBackslash(boolean escapeBackslash) {
this.escapeBackslash = escapeBackslash;
}

/**
* Performs the encoding, sends the encoded sql to the writer passed in.
*
Expand Down Expand Up @@ -529,7 +543,8 @@ public Object visit(PropertyIsLike filter, Object extraData) {
literal += multi;
}

String pattern = LikeFilterImpl.convertToSQL92(esc, multi, single, matchCase, literal);
String pattern =
LikeFilterImpl.convertToSQL92(esc, multi, single, matchCase, literal, false);

try {
if (!matchCase) {
Expand All @@ -539,13 +554,12 @@ public Object visit(PropertyIsLike filter, Object extraData) {
att.accept(this, extraData);

if (!matchCase) {
out.write(") LIKE '");
out.write(") LIKE ");
} else {
out.write(" LIKE '");
out.write(" LIKE ");
}

out.write(pattern);
out.write("' ");
writeLiteral(pattern);
} catch (java.io.IOException ioe) {
throw new RuntimeException(IO_ERROR, ioe);
}
Expand Down Expand Up @@ -1170,11 +1184,8 @@ public Object visit(Id filter, Object extraData) {
out.write(".");
}
out.write(escapeName(columns.get(j).getName()));
out.write(" = '");
out.write(
attValues.get(j).toString()); // DJB: changed this to attValues[j] from
// attValues[i].
out.write("'");
out.write(" = ");
writeLiteral(attValues.get(j));

if (j < (attValues.size() - 1)) {
out.write(" AND ");
Expand Down Expand Up @@ -1747,12 +1758,17 @@ protected void writeLiteral(Object literal) throws IOException {
encoding = literal.toString();
}

// sigle quotes must be escaped to have a valid sql string
String escaped = encoding.replaceAll("'", "''");
// single quotes must be escaped to have a valid sql string
String escaped = escapeLiteral(encoding);
out.write("'" + escaped + "'");
}
}

/** Escapes the string literal. */
public String escapeLiteral(String literal) {
return EscapeSql.escapeLiteral(literal, escapeBackslash, false);
}

/**
* Subclasses must implement this method in order to encode geometry filters according to the
* specific database implementation
Expand Down
Expand Up @@ -16,6 +16,8 @@
*/
package org.geotools.jdbc;

import java.util.regex.Pattern;

/**
* Perform basic SQL validation on input string. This is to allow safe encoding of parameters that
* must contain quotes, while still protecting users from SQL injection.
Expand All @@ -24,6 +26,28 @@
* quotes. Backslashes are too risky to allow so are removed completely
*/
public class EscapeSql {

private static final Pattern SINGLE_QUOTE_PATTERN = Pattern.compile("'");

private static final Pattern DOUBLE_QUOTE_PATTERN = Pattern.compile("\"");

private static final Pattern BACKSLASH_PATTERN = Pattern.compile("\\\\");

public static String escapeLiteral(
String literal, boolean escapeBackslash, boolean escapeDoubleQuote) {
// ' --> ''
String escaped = SINGLE_QUOTE_PATTERN.matcher(literal).replaceAll("''");
if (escapeBackslash) {
// \ --> \\
escaped = BACKSLASH_PATTERN.matcher(escaped).replaceAll("\\\\\\\\");
}
if (escapeDoubleQuote) {
// " --> \"
escaped = DOUBLE_QUOTE_PATTERN.matcher(escaped).replaceAll("\\\\\"");
}
return escaped;
}

public static String escapeSql(String str) {

// ' --> ''
Expand Down
Expand Up @@ -28,6 +28,7 @@
import java.sql.Timestamp;
import java.sql.Types;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -39,6 +40,8 @@
import java.util.logging.Logger;
import org.geotools.data.Join.Type;
import org.geotools.data.Query;
import org.geotools.data.jdbc.datasource.DataSourceFinder;
import org.geotools.data.jdbc.datasource.UnWrapper;
import org.geotools.feature.visitor.AverageVisitor;
import org.geotools.feature.visitor.CountVisitor;
import org.geotools.feature.visitor.FeatureAttributeVisitor;
Expand Down Expand Up @@ -164,6 +167,41 @@ public abstract class SQLDialect {
}
};

/**
* Sentinel value used to mark that the unwrapper lookup happened already, and an unwrapper was
* not found
*/
protected static final UnWrapper UNWRAPPER_NOT_FOUND =
new UnWrapper() {

@Override
public Statement unwrap(Statement statement) {
throw new UnsupportedOperationException();
}

@Override
public Connection unwrap(Connection conn) {
throw new UnsupportedOperationException();
}

@Override
public boolean canUnwrap(Statement st) {
return false;
}

@Override
public boolean canUnwrap(Connection conn) {
return false;
}
};

/**
* Map of {@code UnWrapper} objects keyed by the class of {@code Connection} it is an unwrapper
* for. This avoids the overhead of searching the {@code DataSourceFinder} service registry at
* each unwrap.
*/
protected final Map<Class<? extends Connection>, UnWrapper> uwMap = new HashMap<>();

/** The datastore using the dialect */
protected JDBCDataStore dataStore;

Expand Down Expand Up @@ -1415,4 +1453,52 @@ public boolean canGroupOnGeometry() {
public Class<?> getMapping(String sqlTypeName) {
return null;
}

/** Obtains the native connection object given a database connection. */
@SuppressWarnings("PMD.CloseResource")
protected <T extends Connection> T unwrapConnection(Connection cx, Class<T> clazz)
throws SQLException {
if (clazz.isInstance(cx)) {
return clazz.cast(cx);
}
try {
// Unwrap the connection multiple levels as necessary to get at the underlying
// connection. Maintain a map of UnWrappers to avoid searching the registry
// every time we need to unwrap.
Connection testCon = cx;
Connection toUnwrap;
do {
UnWrapper unwrapper = uwMap.get(testCon.getClass());
if (unwrapper == null) {
unwrapper = DataSourceFinder.getUnWrapper(testCon);
if (unwrapper == null) {
unwrapper = UNWRAPPER_NOT_FOUND;
}
uwMap.put(testCon.getClass(), unwrapper);
}
if (unwrapper == UNWRAPPER_NOT_FOUND) {
// give up and do Java unwrap below
break;
}
toUnwrap = testCon;
testCon = unwrapper.unwrap(testCon);
if (clazz.isInstance(testCon)) {
return clazz.cast(cx);
}
} while (testCon != null && testCon != toUnwrap);
// try to use Java unwrapping
try {
if (cx.isWrapperFor(clazz)) {
return cx.unwrap(clazz);
}
} catch (Throwable t) {
// not a mistake, old DBCP versions will throw an Error here, we need to catch it
LOGGER.log(Level.FINER, "Failed to unwrap connection using Java facilities", t);
}
} catch (IOException e) {
throw new SQLException(
"Could not obtain " + clazz.getName() + " from " + cx.getClass(), e);
}
throw new SQLException("Could not obtain " + clazz.getName() + " from " + cx.getClass());
}
}
Expand Up @@ -465,4 +465,18 @@ public void testEscapeName() {
encoder.setSqlNameEscape("");
Assert.assertEquals("abc", encoder.escapeName("abc"));
}

@Test
public void testLikeEscaping() throws Exception {
Filter filter = ff.like(ff.property("testString"), "\\'FOO", "%", "-", "\\", true);
FilterToSQL encoder = new FilterToSQL(output);
Assert.assertEquals("WHERE testString LIKE '''FOO'", encoder.encodeToString(filter));
}

@Test
public void testIdEscaping() throws Exception {
Id id = ff.id(Collections.singleton(ff.featureId("'FOO")));
encoder.encode(id);
Assert.assertEquals("WHERE (id = '''FOO')", output.toString());
}
}
Expand Up @@ -79,13 +79,49 @@ public class LikeFilterImpl extends AbstractFilter implements PropertyIsLike {
* have a special char as another special char. Using this will throw an error
* (IllegalArgumentException).
*/
@Deprecated
public static String convertToSQL92(
char escape, char multi, char single, boolean matchCase, String pattern)
throws IllegalArgumentException {
return convertToSQL92(escape, multi, single, matchCase, pattern, true);
}

/**
* Given OGC PropertyIsLike Filter information, construct an SQL-compatible 'like' pattern.
*
* <p>SQL % --> match any number of characters _ --> match a single character
*
* <p>NOTE; the SQL command is 'string LIKE pattern [ESCAPE escape-character]' We could
* re-define the escape character, but I'm not doing to do that in this code since some
* databases will not handle this case.
*
* <p>Method: 1.
*
* <p>Examples: ( escape ='!', multi='*', single='.' ) broadway* -> 'broadway%' broad_ay ->
* 'broad_ay' broadway -> 'broadway'
*
* <p>broadway!* -> 'broadway*' (* has no significance and is escaped) can't -> 'can''t' ( '
* escaped for SQL compliance)
*
* <p>NOTE: when the escapeSingleQuote parameter is false, this method will not convert ' to ''
* (double single quote) and it is the caller's responsibility to ensure that the resulting
* pattern is used safely in SQL queries.
*
* <p>NOTE: we dont handle "'" as a 'special' character because it would be too confusing to
* have a special char as another special char. Using this will throw an error
* (IllegalArgumentException).
*/
public static String convertToSQL92(
char escape,
char multi,
char single,
boolean matchCase,
String pattern,
boolean escapeSingleQuote) {
if ((escape == '\'') || (multi == '\'') || (single == '\''))
throw new IllegalArgumentException("do not use single quote (') as special char!");

StringBuffer result = new StringBuffer(pattern.length() + 5);
StringBuilder result = new StringBuilder(pattern.length() + 5);
for (int i = 0; i < pattern.length(); i++) {
char chr = pattern.charAt(i);
if (chr == escape) {
Expand All @@ -96,7 +132,7 @@ public static String convertToSQL92(
result.append('_');
} else if (chr == multi) {
result.append('%');
} else if (chr == '\'') {
} else if (chr == '\'' && escapeSingleQuote) {
result.append('\'');
result.append('\'');
} else {
Expand All @@ -108,6 +144,7 @@ public static String convertToSQL92(
}

/** see convertToSQL92 */
@Deprecated
public String getSQL92LikePattern() throws IllegalArgumentException {
if (escape.length() != 1) {
throw new IllegalArgumentException(
Expand All @@ -126,7 +163,8 @@ public String getSQL92LikePattern() throws IllegalArgumentException {
wildcardMulti.charAt(0),
wildcardSingle.charAt(0),
matchingCase,
pattern);
pattern,
true);
}

public void setWildCard(String wildCard) {
Expand Down
Expand Up @@ -187,29 +187,37 @@ public void setUp() throws SchemaException {
@Test
public void testLikeToSQL() {
Assert.assertEquals(
"BroadWay%", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "BroadWay*"));
"BroadWay%", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "BroadWay*", true));
Assert.assertEquals(
"broad#ay", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broad#ay"));
"broad#ay", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broad#ay", true));
Assert.assertEquals(
"broadway", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broadway"));
"broadway", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broadway", true));

Assert.assertEquals(
"broad_ay", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broad.ay"));
"broad_ay", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broad.ay", true));
Assert.assertEquals(
"broad.ay", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broad!.ay"));
"broad.ay", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broad!.ay", true));

Assert.assertEquals(
"broa''dway", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broa'dway"));
"broa''dway",
LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broa'dway", true));
Assert.assertEquals(
"broa''''dway",
LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broa" + "''dway"));
LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broa" + "''dway", true));
Assert.assertEquals(
"broa'dway",
LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broa'dway", false));
Assert.assertEquals(
"broa''dway",
LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broa" + "''dway", false));

Assert.assertEquals(
"broadway_", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broadway."));
"broadway_", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broadway.", true));
Assert.assertEquals(
"broadway", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broadway!"));
"broadway", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broadway!", true));
Assert.assertEquals(
"broadway!", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broadway!!"));
"broadway!",
LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broadway!!", true));
}

/**
Expand Down

0 comments on commit 64fb4c4

Please sign in to comment.