Skip to content

Commit 070887e

Browse files
authored
Obfuscate SQL params passed by ExecuteSqlCommand and SqlExecuteCommand (#61)
1 parent 173fcf5 commit 070887e

File tree

6 files changed

+79
-15
lines changed

6 files changed

+79
-15
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## version 5.4.0-SNAPSHOT
44
*Released*: TBD
5+
* Obfuscate SQL parameters passed by `ExecuteSqlCommand` and `SqlExecuteCommand` to avoid rejection by web application firewalls
6+
* Earliest compatible LabKey Server version: 23.9.0
7+
* These commands are no longer compatible with earlier versions of LabKey Server (23.8.x and before) by default, however,
8+
if targeting an older server, calling `ExecuteSqlCommand.setWafEncoding(false)` will restore the previous behavior.
9+
* Update HttpCore version
510

611
## version 5.3.0
712
*Released*: 7 September 2023

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ commonsLoggingVersion=1.2
1515
hamcrestVersion=1.3
1616

1717
httpclient5Version=5.2.1
18-
httpcore5Version=5.2.2
18+
httpcore5Version=5.2.3
1919

2020
jsonObjectVersion=20230618
2121

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.labkey.remoteapi.internal;
2+
3+
import java.net.URLEncoder;
4+
import java.nio.charset.StandardCharsets;
5+
import java.util.Base64;
6+
7+
public class EncodeUtils
8+
{
9+
// Text that is first urlencoded to flatten UNICODE to ASCII then base64 encoded to avoid WAF rejection
10+
public static final String WAF_PREFIX = "/*{{base64/x-www-form-urlencoded/wafText}}*/";
11+
12+
/**
13+
* Obfuscates content that's often intercepted by web application firewalls that are scanning for likely SQL or
14+
* script injection. We have a handful of endpoints that intentionally accept SQL or script, so we encode the text
15+
* to avoid tripping alarms. It's a simple BASE64 encoding that obscures the content, and lets the WAF scan for and
16+
* reject malicious content on all other parameters. See issue 48509 and PageFlowUtil.wafEncode()/wafDecode().
17+
*/
18+
public static String wafEncode(String plain)
19+
{
20+
if (null == plain || plain.isBlank())
21+
{
22+
return null;
23+
}
24+
var step1 = encodeURIComponent(plain);
25+
var step2 = step1.getBytes(StandardCharsets.US_ASCII);
26+
return WAF_PREFIX + Base64.getEncoder().encodeToString(step2);
27+
}
28+
29+
/**
30+
* URL Encode string.
31+
* NOTE! this should be used on parts of a url, not an entire url
32+
* Like JavaScript encodeURIComponent()
33+
*/
34+
public static String encodeURIComponent(String s)
35+
{
36+
if (null == s)
37+
return "";
38+
String enc = URLEncoder.encode(s, StandardCharsets.UTF_8);
39+
return enc.replace("+", "%20");
40+
}
41+
}

src/org/labkey/remoteapi/query/ExecuteSqlCommand.java

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import org.json.JSONObject;
1919
import org.labkey.remoteapi.PostCommand;
20+
import org.labkey.remoteapi.internal.EncodeUtils;
2021

2122
import java.util.HashMap;
2223
import java.util.List;
@@ -47,6 +48,7 @@ public class ExecuteSqlCommand extends PostCommand<SelectRowsResponse> implement
4748
private boolean _saveInSession = false;
4849
private boolean _includeDetailsColumn = false;
4950
private Map<String, String> _queryParameters = new HashMap<>();
51+
private boolean _wafEncoding = true;
5052

5153
/**
5254
* Constructs an ExecuteSqlCommand, initialized with a schema name.
@@ -198,7 +200,7 @@ public void setIncludeTotalCount(boolean includeTotalCount)
198200
The value of this property should be a comma-delimited list of column names you want to sort by.
199201
Use a - prefix to sort a column in descending order
200202
(e.g., 'LastName,-Age' to sort first by LastName, then by Age descending).
201-
@return the set of sorts to apply
203+
@return the list of sorts to apply
202204
*/
203205
public List<Sort> getSorts()
204206
{
@@ -219,7 +221,7 @@ public void setSort(List<Sort> sorts)
219221
}
220222

221223
/**
222-
* Whether or not the definition of this query should be stored for reuse during the current session.
224+
* Whether the definition of this query should be stored for reuse during the current session.
223225
* If true, all information required to recreate the query will be stored on the server and a unique query name
224226
* will be passed to the success callback. This temporary query name can be used by all other API methods,
225227
* including Query Web Part creation, for as long as the current user's session remains active.
@@ -231,7 +233,7 @@ public boolean isSaveInSession()
231233
}
232234

233235
/**
234-
* Whether or not the definition of this query should be stored for reuse during the current session.
236+
* Whether the definition of this query should be stored for reuse during the current session.
235237
* If true, all information required to recreate the query will be stored on the server and a unique query name
236238
* will be passed to the success callback. This temporary query name can be used by all other API methods,
237239
* including Query Web Part creation, for as long as the current user's session remains active.
@@ -267,7 +269,7 @@ public void setIncludeDetailsColumn(boolean includeDetailsColumn)
267269
/**
268270
Map of name (string)/value pairs for the values of parameters if the SQL references underlying queries
269271
that are parameterized.
270-
@return the set of query parameters for the SQL references
272+
@return map of query parameters for the SQL references
271273
*/
272274
public Map<String, String> getQueryParameters()
273275
{
@@ -305,6 +307,20 @@ public void setContainerFilter(ContainerFilter containerFilter)
305307
_containerFilter = containerFilter;
306308
}
307309

310+
public boolean getWafEncoding()
311+
{
312+
return _wafEncoding;
313+
}
314+
315+
/**
316+
* By default, this command encodes the SQL parameter to allow it to pass through web application firewalls. This
317+
* is compatible with LabKey Server v23.9.0 and above. If targeting an earlier server, pass false to this method.
318+
*/
319+
public void setWafEncoding(boolean wafEncoding)
320+
{
321+
_wafEncoding = wafEncoding;
322+
}
323+
308324
@Override
309325
protected SelectRowsResponse createResponse(String text, int status, String contentType, JSONObject json)
310326
{
@@ -318,7 +334,7 @@ public JSONObject getJsonObject()
318334
{
319335
JSONObject json = new JSONObject();
320336
json.put("schemaName", getSchemaName());
321-
json.put("sql", getSql());
337+
json.put("sql", getWafEncoding() ? EncodeUtils.wafEncode(getSql()) : getSql());
322338
if (getMaxRows() >= 0)
323339
json.put("maxRows", getMaxRows());
324340
if (getOffset() > 0)
@@ -337,7 +353,7 @@ protected Map<String, Object> createParameterMap()
337353
{
338354
Map<String, Object> params = super.createParameterMap();
339355

340-
if (null != getSorts() && getSorts().size() > 0)
356+
if (null != getSorts() && !getSorts().isEmpty())
341357
params.put("query.sort", Sort.getSortQueryStringParam(getSorts()));
342358

343359
for (Map.Entry<String, String> entry : getQueryParameters().entrySet())

src/org/labkey/remoteapi/query/Filter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,8 @@ public void setValue(Object value)
237237
public String getQueryStringParamName()
238238
{
239239
return (null == getColumnName() || null == getOperator())
240-
? ""
241-
: getColumnName() + "~" + getOperator().getUrlKey();
240+
? ""
241+
: getColumnName() + "~" + getOperator().getUrlKey();
242242
}
243243

244244
/**

src/org/labkey/remoteapi/query/SqlExecuteCommand.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.json.JSONObject;
1919
import org.labkey.remoteapi.CommandResponse;
2020
import org.labkey.remoteapi.PostCommand;
21+
import org.labkey.remoteapi.internal.EncodeUtils;
2122

2223
import java.util.HashMap;
2324
import java.util.Map;
@@ -43,10 +44,10 @@ public class SqlExecuteCommand extends PostCommand<CommandResponse>
4344

4445
private String _schemaName;
4546
private String _sql;
46-
private Map<String, Object> _queryParameters = new HashMap<>();
47-
private String _sep = us_char + "\t";
48-
private String _eol = us_char + "\n";
49-
private boolean _compact = true;
47+
private final Map<String, Object> _queryParameters = new HashMap<>();
48+
private final String _sep = us_char + "\t";
49+
private final String _eol = us_char + "\n";
50+
private final boolean _compact = true;
5051

5152
/**
5253
* Constructs an ExecuteSqlCommand, initialized with a schema name.
@@ -107,7 +108,7 @@ public void setSql(String sql)
107108
/**
108109
Map of name (string)/value pairs for the values of parameters if the SQL references underlying queries
109110
that are parameterized.
110-
@return the set of query parameters for the SQL references
111+
@return map of query parameters for the SQL references
111112
*/
112113
public Map<String, Object> getQueryParameters()
113114
{
@@ -129,6 +130,7 @@ public String getLineSeparator()
129130
{
130131
return _eol;
131132
}
133+
132134
public String getFieldSeparator()
133135
{
134136
return _sep;
@@ -139,7 +141,7 @@ public JSONObject getJsonObject()
139141
{
140142
JSONObject json = new JSONObject();
141143
json.put("schema", getSchemaName());
142-
json.put("sql", getSql());
144+
json.put("sql", EncodeUtils.wafEncode(getSql()));
143145
json.put("parameters", getQueryParameters());
144146
json.put("eol", _eol);
145147
json.put("sep", _sep);

0 commit comments

Comments
 (0)