http://www.myprettyproducts.com/product?filter="type.eq(3).and(prize.lt(500))"The proposed solution leveraged both MVEL and QueryDsl to convert the filter expression at runtime to an executable QueryDsl expression.
As indicated in my post this approach introduced a security vulnerability as we open up our application to code injection via the filter expression. The filter expression is just a string which is interpreted by MVEL as Java code.
The way I defeated this is to restrict the expression string before it is given to MVEL. It is restricted to only accept terms and constructions that lead to a valid QueryDsl Predicate and nothing more. For this purpose I wrote a custom dynamic Parser using Parboild. Parboild is a socalled PEG (Parsing expression grammar) parser.
This is the grammar I came up with to parse the expressions:
FilterExpression : Expression Expression : MethodInvocation ['.'MethodInvocation]+ MethodInvocation : Method ArgumentList Method : Path Path : PathElement ['.'PathElement]+ PathElement : ArgumentList : '(' Argument? ['.'Argument]+ ')' Argument : Literal / Expression / Path / Array Literal : FloatLiteral / IntegerLiteral / CharLiteral / StringLiteral / StringLiteral2 / 'true' / 'false' / 'null' Array : '[' (Literal / [','Literal]+) / (Expression / [','Expression]+) ']'The rule definition of the literal values is copied from an example for parsing of Java source code from the Parboiled website.
public class FilterExpressionParser extends BaseParser<Object> { private String[] allowedPathElements; public FilterExpressionParser(Collection<String> allowedPathElements) { this.allowedPathElements = (String[])allowedPathElements.toArray(new String[0]); Arrays.sort(this.allowedPathElements, new Comparator<String>() { @Override public int compare(java.lang.String o1, java.lang.String o2) { return o2.compareTo(o1); } }); } Rule FilterExpression() { return Sequence(Expression(), EOI); } Rule Expression() { return Sequence(MethodInvocation(), ZeroOrMore(".", MethodInvocation())); } Rule MethodInvocation() { return Sequence(Method(), ArgumentList()); } Rule ArgumentList() { return Sequence("(", Optional(Argument(), ZeroOrMore(",", Argument())), ")"); } Rule Argument() { return FirstOf(Literal(), Expression(), Path(), Array()); } Rule Array() { return Sequence("[", FirstOf(Sequence(Literal(), ZeroOrMore(",", Literal())), Sequence(Expression(), ZeroOrMore(",", Expression()))), "]"); } Rule Method() { return Path(); } Rule Path() { return Sequence(FirstOf(allowedPathElements), ZeroOrMore(".", FirstOf(allowedPathElements))); } @MemoMismatches Rule LetterOrDigit() { // switch to this "reduced" character space version for a ~10% parser performance speedup return FirstOf(CharRange('a', 'z'), CharRange('A', 'Z'), CharRange('0', '9'), '_', '$'); // return FirstOf(Sequence('\\', UnicodeEscape()), new JavaLetterOrDigitMatcher()); } Rule Literal() { return FirstOf(Sequence(Optional("-"), FloatLiteral()), Sequence(Optional("-"), IntegerLiteral()), CharLiteral(), StringLiteral(), StringLiteral2(), Sequence("true", TestNot(LetterOrDigit())), Sequence("false", TestNot(LetterOrDigit())), Sequence("null", TestNot(LetterOrDigit())) ); } @SuppressSubnodes Rule IntegerLiteral() { return Sequence(DecimalNumeral(), Optional(AnyOf("lL"))); } @SuppressSubnodes Rule DecimalNumeral() { return FirstOf('0', Sequence(CharRange('1', '9'), ZeroOrMore(Digit()))); } Rule HexDigit() { return FirstOf(CharRange('a', 'f'), CharRange('A', 'F'), CharRange('0', '9')); } Rule FloatLiteral() { return DecimalFloat(); } @SuppressSubnodes Rule DecimalFloat() { return FirstOf(Sequence(OneOrMore(Digit()), '.', ZeroOrMore(Digit()), Optional(Exponent()), Optional(AnyOf("fFdD"))), Sequence('.', OneOrMore(Digit()), Optional(Exponent()), Optional(AnyOf("fFdD"))), Sequence(OneOrMore(Digit()), Exponent(), Optional(AnyOf("fFdD"))), Sequence(OneOrMore(Digit()), Optional(Exponent()), AnyOf("fFdD"))); } Rule Exponent() { return Sequence(AnyOf("eE"), Optional(AnyOf("+-")), OneOrMore(Digit())); } Rule Digit() { return CharRange('0', '9'); } Rule CharLiteral() { return Sequence('\'', FirstOf(Escape(), Sequence(TestNot(AnyOf("'\\")), ANY)).suppressSubnodes(), '\''); } Rule StringLiteral() { return Sequence('"', ZeroOrMore(FirstOf(Escape(), Sequence(TestNot(AnyOf("\r\n\"\\")), ANY))).suppressSubnodes(), '"'); } Rule StringLiteral2() { return Sequence('\'', ZeroOrMore(FirstOf(Escape(), Sequence(TestNot(AnyOf("\r\n'\\")), ANY))).suppressSubnodes(), '\''); } Rule Escape() { return Sequence('\\', FirstOf(AnyOf("btnfr\"\'\\"), OctalEscape(), UnicodeEscape())); } Rule OctalEscape() { return FirstOf(Sequence(CharRange('0', '3'), CharRange('0', '7'), CharRange('0', '7')), Sequence(CharRange('0', '7'), CharRange('0', '7')), CharRange('0', '7')); } Rule UnicodeEscape() { return Sequence(OneOrMore('u'), HexDigit(), HexDigit(), HexDigit(), HexDigit()); } }You use it like this:
private final static ListIf you want to use an entity property in a filter expression which type is not a primitive type or String, but for instance a java.util.Date object, then you can use a object factory for this. A factory creates an object instance from a string representation.queryDslMethods = Arrays.asList("and", "or", "not", "eq", "ne",
"in", "notIn", "after", "before", "between", "notBetween", "lt", "loe", "gt", "goe",
"equalsIgnoreCase", "like", "matches", "startsWith", "startsWithIgnoreCase"); public Predicate toPredicate() { // create set with all terms that are allowed to appear as 'methods' in a filter expression SetallowedMethods = new HashSet (); set.addAll(queryDslMethods); set.addAll(Arrays.asList("type", "prize", "creationDate")); set.add("_date"); set.add("valueOf"); // needed for ValueFactory calls, i.e. _date.valueOf(...) // create the Parboiled parser with our FilterExpressionParser class FilterExpressionParser parser = Parboiled.createParser(FilterExpressionParser.class, allowedMethods); ReportingParseRunner<Object> parseRunner = new ReportingParseRunner<Object>(parser.FilterExpression()); // run the parser and examine result ParsingResult<Object> parsingResult = parseRunner.run(expression); if (!parsingResult.matched) { throw new IllegalArgumentException("filter expression is invalid: " + expression); } return evalExpression(expression); } private Predicate evalExpression(String expression) // create a map with all Objects that need to be available in the MVEL context. Map<String, Object> vars = new HashMap<String, Object>(); QProduct qProduct = QProduct.product; vars.put("type", qProduct.type); vars.put("prize", qProduct.prize); vars.put("creationDate", qProduct.creationDate); vars.put("_date", new DateFactory()); return (Predicate)MVEL.eval(expression, vars); }
With the following factory you can construct date objects as part of a filter expression:
import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; public class DateFactory { public String getName(){ return "_date"; } public Date valueOf(String value) { DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-mm-dd"); return formatter.parseDateTime(value).toDate(); } }You can see in the example code above the _date and valueof strings are added to the allowedMethods set for parsing the expression. Furthermore a DateFactory instance is put to the vars map with the name _date, so it is available when evalutaion the expression by MVEL. You use a date in a filter expression like this:
http://www.myprettyproducts.com/product?filter="type.eq(3).and(creationDate.after(_date.valueof('2012-05-23')))"Of course you will need to parameterize the above code to make it reusable for different use cases.
1 comment:
Interesting approach, but why do you quote the URL parameter?
Post a Comment