Skip to content

Commit

Permalink
Merge pull request from GHSA-99c3-qc2q-p94m
Browse files Browse the repository at this point in the history
  • 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.function.InFunction;
import org.geotools.filter.spatial.BBOXImpl; import org.geotools.filter.spatial.BBOXImpl;
import org.geotools.jdbc.EnumMapper; import org.geotools.jdbc.EnumMapper;
import org.geotools.jdbc.EscapeSql;
import org.geotools.jdbc.JDBCDataStore; import org.geotools.jdbc.JDBCDataStore;
import org.geotools.jdbc.JoinId; import org.geotools.jdbc.JoinId;
import org.geotools.jdbc.JoinPropertyName; 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 */ /** Whether the encoder should try to encode "in" function into a SQL IN operator */
protected boolean inEncodingEnabled = true; protected boolean inEncodingEnabled = true;


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

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


Expand Down Expand Up @@ -265,6 +269,16 @@ public void setInEncodingEnabled(boolean inEncodingEnabled) {
this.inEncodingEnabled = 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. * 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; literal += multi;
} }


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


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


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


out.write(pattern); writeLiteral(pattern);
out.write("' ");
} catch (java.io.IOException ioe) { } catch (java.io.IOException ioe) {
throw new RuntimeException(IO_ERROR, 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(".");
} }
out.write(escapeName(columns.get(j).getName())); out.write(escapeName(columns.get(j).getName()));
out.write(" = '"); out.write(" = ");
out.write( writeLiteral(attValues.get(j));
attValues.get(j).toString()); // DJB: changed this to attValues[j] from
// attValues[i].
out.write("'");


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


// sigle quotes must be escaped to have a valid sql string // single quotes must be escaped to have a valid sql string
String escaped = encoding.replaceAll("'", "''"); String escaped = escapeLiteral(encoding);
out.write("'" + escaped + "'"); 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 * Subclasses must implement this method in order to encode geometry filters according to the
* specific database implementation * specific database implementation
Expand Down
Expand Up @@ -16,6 +16,8 @@
*/ */
package org.geotools.jdbc; 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 * 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. * 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 * quotes. Backslashes are too risky to allow so are removed completely
*/ */
public class EscapeSql { 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) { public static String escapeSql(String str) {


// ' --> '' // ' --> ''
Expand Down
Expand Up @@ -28,6 +28,7 @@
import java.sql.Timestamp; import java.sql.Timestamp;
import java.sql.Types; import java.sql.Types;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
Expand All @@ -39,6 +40,8 @@
import java.util.logging.Logger; import java.util.logging.Logger;
import org.geotools.data.Join.Type; import org.geotools.data.Join.Type;
import org.geotools.data.Query; 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.AverageVisitor;
import org.geotools.feature.visitor.CountVisitor; import org.geotools.feature.visitor.CountVisitor;
import org.geotools.feature.visitor.FeatureAttributeVisitor; 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 */ /** The datastore using the dialect */
protected JDBCDataStore dataStore; protected JDBCDataStore dataStore;


Expand Down Expand Up @@ -1415,4 +1453,52 @@ public boolean canGroupOnGeometry() {
public Class<?> getMapping(String sqlTypeName) { public Class<?> getMapping(String sqlTypeName) {
return null; 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(""); encoder.setSqlNameEscape("");
Assert.assertEquals("abc", encoder.escapeName("abc")); 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 * have a special char as another special char. Using this will throw an error
* (IllegalArgumentException). * (IllegalArgumentException).
*/ */
@Deprecated
public static String convertToSQL92( public static String convertToSQL92(
char escape, char multi, char single, boolean matchCase, String pattern) char escape, char multi, char single, boolean matchCase, String pattern)
throws IllegalArgumentException { 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 == '\'')) if ((escape == '\'') || (multi == '\'') || (single == '\''))
throw new IllegalArgumentException("do not use single quote (') as special char!"); 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++) { for (int i = 0; i < pattern.length(); i++) {
char chr = pattern.charAt(i); char chr = pattern.charAt(i);
if (chr == escape) { if (chr == escape) {
Expand All @@ -96,7 +132,7 @@ public static String convertToSQL92(
result.append('_'); result.append('_');
} else if (chr == multi) { } else if (chr == multi) {
result.append('%'); result.append('%');
} else if (chr == '\'') { } else if (chr == '\'' && escapeSingleQuote) {
result.append('\''); result.append('\'');
result.append('\''); result.append('\'');
} else { } else {
Expand All @@ -108,6 +144,7 @@ public static String convertToSQL92(
} }


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


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


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


Assert.assertEquals( Assert.assertEquals(
"broa''dway", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broa'dway")); "broa''dway",
LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broa'dway", true));
Assert.assertEquals( Assert.assertEquals(
"broa''''dway", "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( Assert.assertEquals(
"broadway_", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broadway.")); "broadway_", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broadway.", true));
Assert.assertEquals( Assert.assertEquals(
"broadway", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broadway!")); "broadway", LikeFilterImpl.convertToSQL92('!', '*', '.', true, "broadway!", true));
Assert.assertEquals( 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.