From dfb373bae3f9e33e1584f423dd335c10fef07d45 Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Wed, 28 Oct 2020 22:27:15 +0100 Subject: [PATCH 01/12] C++: Modify the syntax of inline expectation comments. The syntax is now $ tag1,tag2=value MISSING: tag3=value3 SPURIOUS: tag4=value4. --- .../TestUtilities/InlineExpectationsTest.qll | 106 ++++++++++++++---- 1 file changed, 83 insertions(+), 23 deletions(-) diff --git a/cpp/ql/test/TestUtilities/InlineExpectationsTest.qll b/cpp/ql/test/TestUtilities/InlineExpectationsTest.qll index 9e30de327f83..8c71ad2c70a9 100644 --- a/cpp/ql/test/TestUtilities/InlineExpectationsTest.qll +++ b/cpp/ql/test/TestUtilities/InlineExpectationsTest.qll @@ -160,20 +160,84 @@ abstract class InlineExpectationsTest extends string { * is treated as part of the expected results, except that the comment may contain a `//` sequence * to treat the remainder of the line as a regular (non-interpreted) comment. */ -private string expectationCommentPattern() { result = "\\s*(\\$(?:[^/]|/[^/])*)(?://.*)?" } +private string expectationCommentPattern() { result = "\\s*\\$((?:[^/]|/[^/])*)(?://.*)?" } /** - * RegEx pattern to match a single expected result, not including the leading `$`. It starts with an - * optional `f+:` or `f-:`, followed by one or more comma-separated tags containing only letters, - * `-`, and `_`, optionally followed by `=` and the expected value. + * The possible columns in an expectation comment. The `TDefaultColumn` branch represents the first + * column in a comment. This column is not precedeeded by a name. `TNamedColumn(name)` represents a + * column containing expected results preceeded by the string `name:`. */ -private string expectationPattern() { - result = "(?:(f(?:\\+|-)):)?((?:[A-Za-z-_]+)(?:\\s*,\\s*[A-Za-z-_]+)*)(?:=(.*))?" +private newtype TColumn = + TDefaultColumn() or + TNamedColumn(string name) { name = ["MISSING", "SPURIOUS"] } + +bindingset[start, content] +private int getEndOfColumnPosition(int start, string content) { + result = + min(string name, int cand | + exists(TNamedColumn(name)) and + cand = content.indexOf(name + ":") and + cand > start + | + cand + ) + or + not exists(string name | + exists(TNamedColumn(name)) and + content.indexOf(name + ":") > start + ) and + result = content.length() +} + +private string getAnExpectation(LineComment comment, TColumn column) { + exists(string content | + content = comment.getContents().regexpCapture(expectationCommentPattern(), 1) and + ( + column = TDefaultColumn() and + exists(int end | + end = getEndOfColumnPosition(0, content) and + result = content.prefix(end).splitAt(" ").trim() + ) + or + exists(string name, int start, int end | + column = TNamedColumn(name) and + start = content.indexOf(name + ":") + name.length() + 1 and + end = getEndOfColumnPosition(start, content) and + result = content.substring(start, end).splitAt(" ").trim() + ) + ) and + result != "" + ) +} + +bindingset[expectation] +private string getATag(string expectation) { + ( + result = expectation.prefix(expectation.indexOf("=")).splitAt(",").trim() + or + not exists(expectation.indexOf("=")) and + result = expectation.splitAt(",").trim() + ) } -private string getAnExpectation(LineComment comment) { - result = comment.getContents().regexpCapture(expectationCommentPattern(), 1).splitAt("$").trim() and - result != "" +bindingset[expectation] +private string getValueForTag(string expectation) { + result = expectation.suffix(expectation.indexOf("=") + 1) +} + +private string getColumnString(TColumn column) { + column = TDefaultColumn() and result = "" + or + column = TNamedColumn(result) +} + +/** + * RegEx pattern to match a single expected result, not including the leading `$`. It consists of one or + * more comma-separated tags containing only letters, `-`, and `_`, optionally followed by `=` and the + * expected value. + */ +private string expectationPattern() { + result = "((?:[A-Za-z-_]+)(?:\\s*,\\s*[A-Za-z-_]+)*)(?:=(.*))?" } private newtype TFailureLocatable = @@ -183,24 +247,19 @@ private newtype TFailureLocatable = test.hasActualResult(location, element, tag, value) } or TValidExpectation(LineComment comment, string tag, string value, string knownFailure) { - exists(string expectation | - expectation = getAnExpectation(comment) and - expectation.regexpMatch(expectationPattern()) and - tag = expectation.regexpCapture(expectationPattern(), 2).splitAt(",").trim() and + exists(string expectation, TColumn column | + expectation = getAnExpectation(comment, column) and + tag = getATag(expectation) and ( - if exists(expectation.regexpCapture(expectationPattern(), 3)) - then value = expectation.regexpCapture(expectationPattern(), 3) + if exists(getValueForTag(expectation)) + then value = getValueForTag(expectation) else value = "" ) and - ( - if exists(expectation.regexpCapture(expectationPattern(), 1)) - then knownFailure = expectation.regexpCapture(expectationPattern(), 1) - else knownFailure = "" - ) + knownFailure = getColumnString(column) ) } or TInvalidExpectation(LineComment comment, string expectation) { - expectation = getAnExpectation(comment) and + expectation = getAnExpectation(comment, _) and not expectation.regexpMatch(expectationPattern()) } @@ -265,16 +324,17 @@ private class ValidExpectation extends Expectation, TValidExpectation { } } +/* Note: These next three classes correspond to all the possible values of type `TColumn`. */ class GoodExpectation extends ValidExpectation { GoodExpectation() { getKnownFailure() = "" } } class FalsePositiveExpectation extends ValidExpectation { - FalsePositiveExpectation() { getKnownFailure() = "f+" } + FalsePositiveExpectation() { getKnownFailure() = "SPURIOUS" } } class FalseNegativeExpectation extends ValidExpectation { - FalseNegativeExpectation() { getKnownFailure() = "f-" } + FalseNegativeExpectation() { getKnownFailure() = "MISSING" } } class InvalidExpectation extends Expectation, TInvalidExpectation { From 176522d0116df92b1cfa59ab59fe430ad4844cc8 Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Wed, 28 Oct 2020 22:28:07 +0100 Subject: [PATCH 02/12] C++: Update terminology in strings and QLDoc. --- .../TestUtilities/InlineExpectationsTest.qll | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/cpp/ql/test/TestUtilities/InlineExpectationsTest.qll b/cpp/ql/test/TestUtilities/InlineExpectationsTest.qll index 8c71ad2c70a9..ad50cc11a8cd 100644 --- a/cpp/ql/test/TestUtilities/InlineExpectationsTest.qll +++ b/cpp/ql/test/TestUtilities/InlineExpectationsTest.qll @@ -43,15 +43,15 @@ * There is no need to write a `select` clause or query predicate. All of the differences between * expected results and actual results will be reported in the `failures()` query predicate. * - * To annotate the test source code with an expected result, place a comment on the + * To annotate the test source code with an expected result, place a comment starting with a `$` on the * same line as the expected result, with text of the following format as the body of the comment: * - * `$tag=expected-value` + * `tag=expected-value` * * Where `tag` is the value of the `tag` parameter from `hasActualResult()`, and `expected-value` is * the value of the `value` parameter from `hasActualResult()`. The `=expected-value` portion may be * omitted, in which case `expected-value` is treated as the empty string. Multiple expectations may - * be placed in the same comment, as long as each is prefixed by a `$`. Any actual result that + * be placed in the same comment. Any actual result that * appears on a line that does not contain a matching expected result comment will be reported with * a message of the form "Unexpected result: tag=value". Any expected result comment for which there * is no matching actual result will be reported with a message of the form @@ -60,30 +60,33 @@ * Example: * ```cpp * int i = x + 5; // $const=5 - * int j = y + (7 - 3) // $const=7 $const=3 $const=4 // The result of the subtraction is a constant. + * int j = y + (7 - 3) // $const=7 const=3 const=4 // The result of the subtraction is a constant. * ``` * - * For tests that contain known false positives and false negatives, it is possible to further - * annotate that a particular expected result is known to be a false positive, or that a particular - * missing result is known to be a false negative: + * For tests that contain known missing and spurious results, it is possible to further + * annotate that a particular expected result is known to be spurious, or that a particular + * missing result is known to be missing: * - * `$f+:tag=expected-value` // False positive - * `$f-:tag=expected-value` // False negative + * `$ SPURIOUS: tag=expected-value` // Spurious result + * `$ MISSING: tag=expected-value` // Missing result * - * A false positive expectation is treated as any other expected result, except that if there is no - * matching actual result, the message will be of the form "Fixed false positive: tag=value". A - * false negative expectation is treated as if there were no expected result, except that if a + * A spurious expectation is treated as any other expected result, except that if there is no + * matching actual result, the message will be of the form "Fixed spurious result: tag=value". A + * missing expectation is treated as if there were no expected result, except that if a * matching expected result is found, the message will be of the form - * "Fixed false negative: tag=value". + * "Fixed missing result: tag=value". + * + * A single line can contain all the expected, spurious and missing results of that line. For instance: + * `$ tag1=value1 SPURIOUS: tag2=value2 MISSING: tag3=value3`. * * If the same result value is expected for two or more tags on the same line, there is a shorthand * notation available: * - * `$tag1,tag2=expected-value` + * `tag1,tag2=expected-value` * * is equivalent to: * - * `$tag1=expected-value $tag2=expected-value` + * `tag1=expected-value tag2=expected-value` */ private import InlineExpectationsTestPrivate @@ -126,7 +129,7 @@ abstract class InlineExpectationsTest extends string { ( exists(FalseNegativeExpectation falseNegative | falseNegative.matchesActualResult(actualResult) and - message = "Fixed false negative:" + falseNegative.getExpectationText() + message = "Fixed missing result:" + falseNegative.getExpectationText() ) or not exists(ValidExpectation expectation | expectation.matchesActualResult(actualResult)) and @@ -143,7 +146,7 @@ abstract class InlineExpectationsTest extends string { message = "Missing result:" + expectation.getExpectationText() or expectation instanceof FalsePositiveExpectation and - message = "Fixed false positive:" + expectation.getExpectationText() + message = "Fixed spurious result:" + expectation.getExpectationText() ) ) or From 4be02a946338dd5f640cb44a2e30428f4bf0f60e Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Thu, 29 Oct 2020 08:48:37 +0100 Subject: [PATCH 03/12] C++: Use new syntax in field-flow tests --- .../test/library-tests/dataflow/fields/A.cpp | 28 +++++++------- .../test/library-tests/dataflow/fields/B.cpp | 4 +- .../test/library-tests/dataflow/fields/C.cpp | 8 ++-- .../test/library-tests/dataflow/fields/D.cpp | 4 +- .../test/library-tests/dataflow/fields/E.cpp | 6 +-- .../dataflow/fields/aliasing.cpp | 38 +++++++++---------- .../library-tests/dataflow/fields/arrays.cpp | 22 +++++------ .../dataflow/fields/by_reference.cpp | 30 +++++++-------- .../library-tests/dataflow/fields/complex.cpp | 4 +- .../dataflow/fields/constructors.cpp | 4 +- .../dataflow/fields/qualifiers.cpp | 12 +++--- .../dataflow/fields/realistic.cpp | 6 +-- .../library-tests/dataflow/fields/simple.cpp | 10 ++--- .../dataflow/fields/struct_init.c | 8 ++-- 14 files changed, 92 insertions(+), 92 deletions(-) diff --git a/cpp/ql/test/library-tests/dataflow/fields/A.cpp b/cpp/ql/test/library-tests/dataflow/fields/A.cpp index d65e900af960..d52ca067c6f7 100644 --- a/cpp/ql/test/library-tests/dataflow/fields/A.cpp +++ b/cpp/ql/test/library-tests/dataflow/fields/A.cpp @@ -40,21 +40,21 @@ class A cc.insert(nullptr); ct.insert(new C()); sink(&cc); // no flow - sink(&ct); // $ast $f-:ir + sink(&ct); // $ ast MISSING: ir } void f1() { C *c = new C(); B *b = B::make(c); - sink(b->c); // $ast $f-:ir + sink(b->c); // $ast MISSING: ir } void f2() { B *b = new B(); b->set(new C1()); - sink(b->get()); // $ast $ir=55:12 - sink((new B(new C()))->get()); // $ast $ir + sink(b->get()); // $ ast ir=55:12 + sink((new B(new C()))->get()); // $ ast ir } void f3() @@ -63,7 +63,7 @@ class A B *b2; b2 = setOnB(b1, new C2()); sink(b1->c); // no flow - sink(b2->c); // $ast $f-:ir + sink(b2->c); // $ ast MISSING: ir } void f4() @@ -72,7 +72,7 @@ class A B *b2; b2 = setOnBWrap(b1, new C2()); sink(b1->c); // no flow - sink(b2->c); // $ast $f-:ir + sink(b2->c); // $ ast MISSING: ir } B *setOnBWrap(B *b1, C *c) @@ -104,7 +104,7 @@ class A { if (C1 *c1 = dynamic_cast(c)) { - sink(c1->a); // $ast,ir + sink(c1->a); // $ ast,ir } C *cc; if (C2 *c2 = dynamic_cast(c)) @@ -117,7 +117,7 @@ class A } if (C1 *c1 = dynamic_cast(cc)) { - sink(c1->a); //$f+:ast + sink(c1->a); // $ SPURIOUS: ast } } @@ -129,7 +129,7 @@ class A { B *b = new B(); f7(b); - sink(b->c); // $ast,ir + sink(b->c); // $ ast,ir } class D @@ -149,9 +149,9 @@ class A { B *b = new B(); D *d = new D(b, r()); - sink(d->b); // $ast,ir=143:25 $ast,ir=150:12 - sink(d->b->c); // $ast $f-:ir - sink(b->c); // $ast,ir + sink(d->b); // $ ast,ir=143:25 ast,ir=150:12 + sink(d->b->c); // $ ast MISSING: ir + sink(b->c); // $ ast,ir } void f10() @@ -162,11 +162,11 @@ class A MyList *l3 = new MyList(nullptr, l2); sink(l3->head); // no flow, b is nested beneath at least one ->next sink(l3->next->head); // no flow - sink(l3->next->next->head); // $ast $f-:ir + sink(l3->next->next->head); // $ ast MISSING: ir sink(l3->next->next->next->head); // no flow for (MyList *l = l3; l != nullptr; l = l->next) { - sink(l->head); // $ast $f-:ir + sink(l->head); // $ ast MISSING: ir } } diff --git a/cpp/ql/test/library-tests/dataflow/fields/B.cpp b/cpp/ql/test/library-tests/dataflow/fields/B.cpp index 01a3f0317cca..3df917afa0de 100644 --- a/cpp/ql/test/library-tests/dataflow/fields/B.cpp +++ b/cpp/ql/test/library-tests/dataflow/fields/B.cpp @@ -6,7 +6,7 @@ class B Elem *e = new Elem(); Box1 *b1 = new Box1(e, nullptr); Box2 *b2 = new Box2(b1); - sink(b2->box1->elem1); // $ast $f-:ir + sink(b2->box1->elem1); // $ ast MISSING: ir sink(b2->box1->elem2); // no flow } @@ -16,7 +16,7 @@ class B Box1 *b1 = new B::Box1(nullptr, e); Box2 *b2 = new Box2(b1); sink(b2->box1->elem1); // no flow - sink(b2->box1->elem2); // $ast $f-:ir + sink(b2->box1->elem2); // $ ast MISSING: ir } static void sink(void *o) {} diff --git a/cpp/ql/test/library-tests/dataflow/fields/C.cpp b/cpp/ql/test/library-tests/dataflow/fields/C.cpp index 892d298a81d0..79b9ffefdeaf 100644 --- a/cpp/ql/test/library-tests/dataflow/fields/C.cpp +++ b/cpp/ql/test/library-tests/dataflow/fields/C.cpp @@ -26,10 +26,10 @@ class C void func() { - sink(s1); // $ast $ir - sink(s2); // $f-:ast $f-:ir - sink(s3); // $ast $ir - sink(s4); // $f-:ast $f-:ir + sink(s1); // $ast ir + sink(s2); // $ MISSING: ast,ir + sink(s3); // $ast ir + sink(s4); // $ MISSING: ast,ir } static void sink(const void *o) {} diff --git a/cpp/ql/test/library-tests/dataflow/fields/D.cpp b/cpp/ql/test/library-tests/dataflow/fields/D.cpp index 6ea3114199eb..86c408388509 100644 --- a/cpp/ql/test/library-tests/dataflow/fields/D.cpp +++ b/cpp/ql/test/library-tests/dataflow/fields/D.cpp @@ -19,7 +19,7 @@ class D { }; static void sinkWrap(Box2* b2) { - sink(b2->getBox1()->getElem()); // $ast=28:15 $ast=35:15 $ast=42:15 $ast=49:15 $f-:ir + sink(b2->getBox1()->getElem()); // $ast=28:15 ast=35:15 ast=42:15 ast=49:15 MISSING: ir } Box2* boxfield; @@ -61,6 +61,6 @@ class D { private: void f5b() { - sink(boxfield->box->elem); // $ast $f-:ir + sink(boxfield->box->elem); // $ ast MISSING: ir } }; diff --git a/cpp/ql/test/library-tests/dataflow/fields/E.cpp b/cpp/ql/test/library-tests/dataflow/fields/E.cpp index 03c83f76bed2..f2349e007946 100644 --- a/cpp/ql/test/library-tests/dataflow/fields/E.cpp +++ b/cpp/ql/test/library-tests/dataflow/fields/E.cpp @@ -18,7 +18,7 @@ void sink(char *b); void handlePacket(packet *p) { - sink(p->data.buffer); // $ast $f-:ir + sink(p->data.buffer); // $ ast MISSING: ir } void f(buf* b) @@ -28,7 +28,7 @@ void f(buf* b) argument_source(raw); argument_source(b->buffer); argument_source(p.data.buffer); - sink(raw); // $ast $f-:ir - sink(b->buffer); // $ast $f-:ir + sink(raw); // $ ast MISSING: ir + sink(b->buffer); // $ ast MISSING: ir handlePacket(&p); } \ No newline at end of file diff --git a/cpp/ql/test/library-tests/dataflow/fields/aliasing.cpp b/cpp/ql/test/library-tests/dataflow/fields/aliasing.cpp index df33dbb288e2..500bbed53a9f 100644 --- a/cpp/ql/test/library-tests/dataflow/fields/aliasing.cpp +++ b/cpp/ql/test/library-tests/dataflow/fields/aliasing.cpp @@ -26,8 +26,8 @@ void callSetters() { referenceSetter(s2); copySetter(s3); - sink(s1.m1); // $ast,ir - sink(s2.m1); // $ast,ir + sink(s1.m1); // $ ast,ir + sink(s2.m1); // $ ast,ir sink(s3.m1); // no flow } @@ -35,12 +35,12 @@ void assignAfterAlias() { S s1 = { 0, 0 }; S &ref1 = s1; ref1.m1 = user_input(); - sink(s1.m1); // $f-:ast $ir + sink(s1.m1); // $ ir MISSING: ast S s2 = { 0, 0 }; S &ref2 = s2; s2.m1 = user_input(); - sink(ref2.m1); // $f-:ast $ir + sink(ref2.m1); // $ ir MISSING: ast } void assignAfterCopy() { @@ -59,7 +59,7 @@ void assignBeforeCopy() { S s2 = { 0, 0 }; s2.m1 = user_input(); S copy2 = s2; - sink(copy2.m1); // $ast,ir + sink(copy2.m1); // $ ast,ir } struct Wrapper { @@ -77,20 +77,20 @@ void pointerIntermediate() { Wrapper w = { { 0, 0 } }; S *s = &w.s; s->m1 = user_input(); - sink(w.s.m1); // $f-:ast $ir + sink(w.s.m1); // $ ir MISSING: ast } void referenceIntermediate() { Wrapper w = { { 0, 0 } }; S &s = w.s; s.m1 = user_input(); - sink(w.s.m1); // $f-:ast $ir + sink(w.s.m1); // $ ir MISSING: ast } void nestedAssign() { Wrapper w = { { 0, 0 } }; w.s.m1 = user_input(); - sink(w.s.m1); // $ast,ir + sink(w.s.m1); // $ ast,ir } void addressOfField() { @@ -99,7 +99,7 @@ void addressOfField() { S s_copy = s; int* px = &s_copy.m1; - sink(*px); // $f-:ast $ir + sink(*px); // $ ir MISSING: ast } void taint_a_ptr(int* pa) { @@ -119,28 +119,28 @@ struct S_with_pointer { void pointer_deref(int* xs) { taint_a_ptr(xs); - sink(xs[0]); // $f-:ast $ir + sink(xs[0]); // $ ir MISSING: ast } void pointer_deref_sub(int* xs) { taint_a_ptr(xs - 2); - sink(*(xs - 2)); // $f-:ast $ir + sink(*(xs - 2)); // $ ir MISSING: ast } void pointer_many_addrof_and_deref(int* xs) { taint_a_ptr(xs); - sink(*&*&*xs); // $f-:ast $ir + sink(*&*&*xs); // $ ir MISSING: ast } void pointer_unary_plus(int* xs) { taint_a_ptr(+xs); - sink(*+xs); // $f-:ast $ir + sink(*+xs); // $ ir MISSING: ast } void pointer_member_index(S_with_pointer s) { taint_a_ptr(s.data); // `s.data` is points to all-aliased-memory - sink(s.data[0]); // $f-:ast,ir + sink(s.data[0]); // $ MISSING: ir,ast } void member_array_different_field(S_with_pointer* s) { @@ -156,13 +156,13 @@ struct S_with_array { void pointer_member_deref() { S_with_array s; taint_a_ptr(s.data); - sink(*s.data); // $ir,ast + sink(*s.data); // $ ir,ast } void array_member_deref() { S_with_array s; taint_a_ptr(s.data); - sink(s.data[0]); // $ir,ast + sink(s.data[0]); // $ ir,ast } struct S2 { @@ -173,7 +173,7 @@ struct S2 { void deep_member_field_dot() { S2 s2; taint_a_ptr(&s2.s.m1); - sink(s2.s.m1); // $ir,ast + sink(s2.s.m1); // $ ir,ast } void deep_member_field_dot_different_fields() { @@ -186,7 +186,7 @@ void deep_member_field_dot_2() { S2 s2; taint_a_ptr(&s2.s.m1); S2 s2_2 = s2; - sink(s2_2.s.m1); // $ir,ast + sink(s2_2.s.m1); // $ ir,ast } void deep_member_field_dot_different_fields_2() { @@ -198,7 +198,7 @@ void deep_member_field_dot_different_fields_2() { void deep_member_field_arrow(S2 *ps2) { taint_a_ptr(&ps2->s.m1); - sink(ps2->s.m1); // $ir,ast + sink(ps2->s.m1); // $ ir,ast } void deep_member_field_arrow_different_fields(S2 *ps2) { diff --git a/cpp/ql/test/library-tests/dataflow/fields/arrays.cpp b/cpp/ql/test/library-tests/dataflow/fields/arrays.cpp index 68290dc70770..8c4f0b34c4d7 100644 --- a/cpp/ql/test/library-tests/dataflow/fields/arrays.cpp +++ b/cpp/ql/test/library-tests/dataflow/fields/arrays.cpp @@ -4,17 +4,17 @@ void *user_input(void); void local_array() { void *arr[10] = { 0 }; arr[0] = user_input(); - sink(arr[0]); // $ast,ir - sink(arr[1]); // $f+:ast - sink(*arr); // $ast,ir - sink(*&arr[0]); // $ast,ir + sink(arr[0]); // $ ast,ir + sink(arr[1]); // $ SPURIOUS: ast + sink(*arr); // $ ast,ir + sink(*&arr[0]); // $ ast,ir } void local_array_convoluted_assign() { void *arr[10] = { 0 }; *&arr[0] = user_input(); - sink(arr[0]); // $ast,ir - sink(arr[1]); // $f+:ast + sink(arr[0]); // $ ast,ir + sink(arr[1]); // $ SPURIOUS: ast } struct inner { @@ -34,18 +34,18 @@ struct outer { void nested_array_1(outer o) { o.nested.arr[1].data = user_input(); - sink(o.nested.arr[1].data); // $ast,ir - sink(o.nested.arr[0].data); // $f+:ast + sink(o.nested.arr[1].data); // $ ast,ir + sink(o.nested.arr[0].data); // $ SPURIOUS: ast } void nested_array_2(outer o) { o.indirect->arr[1].data = user_input(); - sink(o.indirect->arr[1].data); // $ast $f-:ir - sink(o.indirect->arr[0].data); // $f+:ast + sink(o.indirect->arr[1].data); // $ ast MISSING: ir + sink(o.indirect->arr[0].data); // $ SPURIOUS: ast } void nested_array_3(outer o) { o.indirect->ptr[1].data = user_input(); - sink(o.indirect->ptr[1].data); // $f-:ast,ir + sink(o.indirect->ptr[1].data); // $ MISSING: ir,ast sink(o.indirect->ptr[0].data); } diff --git a/cpp/ql/test/library-tests/dataflow/fields/by_reference.cpp b/cpp/ql/test/library-tests/dataflow/fields/by_reference.cpp index 84c3b039cb90..8e84d39be3b4 100644 --- a/cpp/ql/test/library-tests/dataflow/fields/by_reference.cpp +++ b/cpp/ql/test/library-tests/dataflow/fields/by_reference.cpp @@ -48,19 +48,19 @@ struct S { void test_setDirectly() { S s; s.setDirectly(user_input()); - sink(s.getDirectly()); // $ast $ir + sink(s.getDirectly()); // $ast ir } void test_setIndirectly() { S s; s.setIndirectly(user_input()); - sink(s.getIndirectly()); // $ast $ir + sink(s.getIndirectly()); // $ast ir } void test_setThroughNonMember() { S s; s.setThroughNonMember(user_input()); - sink(s.getThroughNonMember()); // $ast $ir + sink(s.getThroughNonMember()); // $ast ir } void test_nonMemberSetA() { @@ -107,13 +107,13 @@ void test_outer_with_ptr(Outer *pouter) { taint_inner_a_ptr(pouter->inner_ptr); taint_a_ptr(&pouter->a); - sink(outer.inner_nested.a); // $ast,ir - sink(outer.inner_ptr->a); // $ast $f-:ir - sink(outer.a); // $ast,ir + sink(outer.inner_nested.a); // $ ast,ir + sink(outer.inner_ptr->a); // $ ast MISSING: ir + sink(outer.a); // $ ast,ir - sink(pouter->inner_nested.a); // $ast,ir - sink(pouter->inner_ptr->a); // $ast $f-:ir - sink(pouter->a); // $ast,ir + sink(pouter->inner_nested.a); // $ ast,ir + sink(pouter->inner_ptr->a); // $ast MISSING: ir + sink(pouter->a); // $ ast,ir } void test_outer_with_ref(Outer *pouter) { @@ -127,11 +127,11 @@ void test_outer_with_ref(Outer *pouter) { taint_inner_a_ref(*pouter->inner_ptr); taint_a_ref(pouter->a); - sink(outer.inner_nested.a); // $ast,ir - sink(outer.inner_ptr->a); // $ast $f-:ir - sink(outer.a); // $ast,ir + sink(outer.inner_nested.a); // $ ast,ir + sink(outer.inner_ptr->a); // $ ast MISSING: ir + sink(outer.a); // $ ast,ir - sink(pouter->inner_nested.a); // $ast,ir - sink(pouter->inner_ptr->a); // $ast $f-:ir - sink(pouter->a); // $ast,ir + sink(pouter->inner_nested.a); // $ ast,ir + sink(pouter->inner_ptr->a); // $ ast MISSING: ir + sink(pouter->a); // $ ast,ir } diff --git a/cpp/ql/test/library-tests/dataflow/fields/complex.cpp b/cpp/ql/test/library-tests/dataflow/fields/complex.cpp index cb55bdd28637..cc5e575859c4 100644 --- a/cpp/ql/test/library-tests/dataflow/fields/complex.cpp +++ b/cpp/ql/test/library-tests/dataflow/fields/complex.cpp @@ -39,8 +39,8 @@ void sink(int x) void bar(Outer &b) { - sink(b.inner.f.a()); // $ast=53:19 $ast=55:19 $ir=53:19 $ir=55:19 - sink(b.inner.f.b()); // $ast=54:19 $ast=56:19 $ir=54:19 $ir=56:19 + sink(b.inner.f.a()); // $ ast=53:19 ast=55:19 ir=53:19 ir=55:19 + sink(b.inner.f.b()); // $ ast=54:19 ast=56:19 ir=54:19 ir=56:19 } void foo() diff --git a/cpp/ql/test/library-tests/dataflow/fields/constructors.cpp b/cpp/ql/test/library-tests/dataflow/fields/constructors.cpp index 4816180954ea..5127e7687c3f 100644 --- a/cpp/ql/test/library-tests/dataflow/fields/constructors.cpp +++ b/cpp/ql/test/library-tests/dataflow/fields/constructors.cpp @@ -25,8 +25,8 @@ class Foo void bar(Foo &f) { - sink(f.a()); //$ast=34:11 $ast=36:11 $ir=34:11 $ir=36:11 - sink(f.b()); //$ast=35:14 $ast=36:25 $ir=35:14 $ir=36:25 + sink(f.a()); // $ ast=34:11 ast=36:11 ir=34:11 ir=36:11 + sink(f.b()); // $ ast=35:14 ast=36:25 ir=35:14 ir=36:25 } void foo() diff --git a/cpp/ql/test/library-tests/dataflow/fields/qualifiers.cpp b/cpp/ql/test/library-tests/dataflow/fields/qualifiers.cpp index dd7baac2933a..18798c1f5fe0 100644 --- a/cpp/ql/test/library-tests/dataflow/fields/qualifiers.cpp +++ b/cpp/ql/test/library-tests/dataflow/fields/qualifiers.cpp @@ -20,31 +20,31 @@ namespace qualifiers { void assignToGetter(Outer outer) { outer.getInner()->a = user_input(); - sink(outer.inner->a); // $ast $f-:ir + sink(outer.inner->a); // $ ast MISSING: ir } void getterArgument1(Outer outer) { outer.getInner()->setA(user_input()); - sink(outer.inner->a); // $ast $f-:ir + sink(outer.inner->a); // $ ast MISSING: ir } void getterArgument2(Outer outer) { pointerSetA(outer.getInner(), user_input()); - sink(outer.inner->a); // $ast $f-:ir + sink(outer.inner->a); // $ ast MISSING: ir } void getterArgument2Ref(Outer outer) { referenceSetA(*outer.getInner(), user_input()); - sink(outer.inner->a); // $ast $f-:ir + sink(outer.inner->a); // $ ast MISSING: ir } void assignToGetterStar(Outer outer) { (*outer.getInner()).a = user_input(); - sink(outer.inner->a); // $ast $f-:ir + sink(outer.inner->a); // $ ast MISSING: ir } void assignToGetterAmp(Outer outer) { (&outer)->getInner()->a = user_input(); - sink(outer.inner->a); // $ast $f-:ir + sink(outer.inner->a); // $ ast MISSING: ir } } \ No newline at end of file diff --git a/cpp/ql/test/library-tests/dataflow/fields/realistic.cpp b/cpp/ql/test/library-tests/dataflow/fields/realistic.cpp index f121dc58b4d7..89f1b4f6248d 100644 --- a/cpp/ql/test/library-tests/dataflow/fields/realistic.cpp +++ b/cpp/ql/test/library-tests/dataflow/fields/realistic.cpp @@ -58,12 +58,12 @@ int main(int argc, char** argv) { return -1; } memcpy(dst, foo.bar[i].baz->userInput.buffer, foo.bar[i].baz->userInput.bufferLen); - sink((void*)foo.bar[i].baz->userInput.bufferLen); // $ast $f-:ir + sink((void*)foo.bar[i].baz->userInput.bufferLen); // $ ast MISSING: ir // There is no flow to the following two `sink` calls because the // source is the _pointer_ returned by `user_input` rather than the // _data_ to which it points. - sink((void*)foo.bar[i].baz->userInput.buffer); // $f-:ast,ir - sink((void*)dst); // $f-:ast,ir + sink((void*)foo.bar[i].baz->userInput.buffer); // $ MISSING: ir,ast + sink((void*)dst); // ir MISSING: ast i++; } return 0; diff --git a/cpp/ql/test/library-tests/dataflow/fields/simple.cpp b/cpp/ql/test/library-tests/dataflow/fields/simple.cpp index 3f7c91f77471..e4d4f70edb0a 100644 --- a/cpp/ql/test/library-tests/dataflow/fields/simple.cpp +++ b/cpp/ql/test/library-tests/dataflow/fields/simple.cpp @@ -25,8 +25,8 @@ class Foo void bar(Foo &f) { - sink(f.a()); //$ast=39:12 $ast=41:12 $ir=39:12 $ir=41:12 - sink(f.b()); //$ast=40:12 $ast=42:12 $ir=40:12 $ir=42:12 + sink(f.a()); // $ ast=39:12 ast=41:12 ir=39:12 ir=41:12 + sink(f.b()); // $ ast=40:12 ast=42:12 ir=40:12 ir=42:12 } void foo() @@ -64,7 +64,7 @@ void single_field_test() A a; a.i = user_input(); A a2 = a; - sink(a2.i); //$ast,ir + sink(a2.i); //$ ast,ir } struct C { @@ -81,7 +81,7 @@ struct C2 void m() { f2.f1 = user_input(); - sink(getf2f1()); //$ast,ir + sink(getf2f1()); //$ ast,ir } }; @@ -91,7 +91,7 @@ void single_field_test_typedef(A_typedef a) { a.i = user_input(); A_typedef a2 = a; - sink(a2.i); //$ast,ir + sink(a2.i); //$ ast,ir } } // namespace Simple diff --git a/cpp/ql/test/library-tests/dataflow/fields/struct_init.c b/cpp/ql/test/library-tests/dataflow/fields/struct_init.c index 891be43cab24..862a3ed42828 100644 --- a/cpp/ql/test/library-tests/dataflow/fields/struct_init.c +++ b/cpp/ql/test/library-tests/dataflow/fields/struct_init.c @@ -12,14 +12,14 @@ struct Outer { }; void absink(struct AB *ab) { - sink(ab->a); //$ast,ir=20:20 $ast,ir=27:7 $ast=40:20 $f-:ir + sink(ab->a); //$ ast,ir=20:20 ast,ir=27:7 ast=40:20 MISSING: ir sink(ab->b); // no flow } int struct_init(void) { struct AB ab = { user_input(), 0 }; - sink(ab.a); //$ast,ir + sink(ab.a); //$ ast,ir sink(ab.b); // no flow absink(&ab); @@ -28,9 +28,9 @@ int struct_init(void) { &ab, }; - sink(outer.nestedAB.a); //$ast,ir + sink(outer.nestedAB.a); //$ ast,ir sink(outer.nestedAB.b); // no flow - sink(outer.pointerAB->a); //$ast $f-:ir + sink(outer.pointerAB->a); //$ ast MISSING: ir sink(outer.pointerAB->b); // no flow absink(&outer.nestedAB); From acf6ffb990aac6a428ce5627e7ff5aaa7323baba Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Thu, 29 Oct 2020 19:07:10 +0100 Subject: [PATCH 04/12] Python: Sync identical file --- .../TestUtilities/InlineExpectationsTest.qll | 143 +++++++++++++----- 1 file changed, 103 insertions(+), 40 deletions(-) diff --git a/python/ql/test/TestUtilities/InlineExpectationsTest.qll b/python/ql/test/TestUtilities/InlineExpectationsTest.qll index 9e30de327f83..ad50cc11a8cd 100644 --- a/python/ql/test/TestUtilities/InlineExpectationsTest.qll +++ b/python/ql/test/TestUtilities/InlineExpectationsTest.qll @@ -43,15 +43,15 @@ * There is no need to write a `select` clause or query predicate. All of the differences between * expected results and actual results will be reported in the `failures()` query predicate. * - * To annotate the test source code with an expected result, place a comment on the + * To annotate the test source code with an expected result, place a comment starting with a `$` on the * same line as the expected result, with text of the following format as the body of the comment: * - * `$tag=expected-value` + * `tag=expected-value` * * Where `tag` is the value of the `tag` parameter from `hasActualResult()`, and `expected-value` is * the value of the `value` parameter from `hasActualResult()`. The `=expected-value` portion may be * omitted, in which case `expected-value` is treated as the empty string. Multiple expectations may - * be placed in the same comment, as long as each is prefixed by a `$`. Any actual result that + * be placed in the same comment. Any actual result that * appears on a line that does not contain a matching expected result comment will be reported with * a message of the form "Unexpected result: tag=value". Any expected result comment for which there * is no matching actual result will be reported with a message of the form @@ -60,30 +60,33 @@ * Example: * ```cpp * int i = x + 5; // $const=5 - * int j = y + (7 - 3) // $const=7 $const=3 $const=4 // The result of the subtraction is a constant. + * int j = y + (7 - 3) // $const=7 const=3 const=4 // The result of the subtraction is a constant. * ``` * - * For tests that contain known false positives and false negatives, it is possible to further - * annotate that a particular expected result is known to be a false positive, or that a particular - * missing result is known to be a false negative: + * For tests that contain known missing and spurious results, it is possible to further + * annotate that a particular expected result is known to be spurious, or that a particular + * missing result is known to be missing: * - * `$f+:tag=expected-value` // False positive - * `$f-:tag=expected-value` // False negative + * `$ SPURIOUS: tag=expected-value` // Spurious result + * `$ MISSING: tag=expected-value` // Missing result * - * A false positive expectation is treated as any other expected result, except that if there is no - * matching actual result, the message will be of the form "Fixed false positive: tag=value". A - * false negative expectation is treated as if there were no expected result, except that if a + * A spurious expectation is treated as any other expected result, except that if there is no + * matching actual result, the message will be of the form "Fixed spurious result: tag=value". A + * missing expectation is treated as if there were no expected result, except that if a * matching expected result is found, the message will be of the form - * "Fixed false negative: tag=value". + * "Fixed missing result: tag=value". + * + * A single line can contain all the expected, spurious and missing results of that line. For instance: + * `$ tag1=value1 SPURIOUS: tag2=value2 MISSING: tag3=value3`. * * If the same result value is expected for two or more tags on the same line, there is a shorthand * notation available: * - * `$tag1,tag2=expected-value` + * `tag1,tag2=expected-value` * * is equivalent to: * - * `$tag1=expected-value $tag2=expected-value` + * `tag1=expected-value tag2=expected-value` */ private import InlineExpectationsTestPrivate @@ -126,7 +129,7 @@ abstract class InlineExpectationsTest extends string { ( exists(FalseNegativeExpectation falseNegative | falseNegative.matchesActualResult(actualResult) and - message = "Fixed false negative:" + falseNegative.getExpectationText() + message = "Fixed missing result:" + falseNegative.getExpectationText() ) or not exists(ValidExpectation expectation | expectation.matchesActualResult(actualResult)) and @@ -143,7 +146,7 @@ abstract class InlineExpectationsTest extends string { message = "Missing result:" + expectation.getExpectationText() or expectation instanceof FalsePositiveExpectation and - message = "Fixed false positive:" + expectation.getExpectationText() + message = "Fixed spurious result:" + expectation.getExpectationText() ) ) or @@ -160,20 +163,84 @@ abstract class InlineExpectationsTest extends string { * is treated as part of the expected results, except that the comment may contain a `//` sequence * to treat the remainder of the line as a regular (non-interpreted) comment. */ -private string expectationCommentPattern() { result = "\\s*(\\$(?:[^/]|/[^/])*)(?://.*)?" } +private string expectationCommentPattern() { result = "\\s*\\$((?:[^/]|/[^/])*)(?://.*)?" } /** - * RegEx pattern to match a single expected result, not including the leading `$`. It starts with an - * optional `f+:` or `f-:`, followed by one or more comma-separated tags containing only letters, - * `-`, and `_`, optionally followed by `=` and the expected value. + * The possible columns in an expectation comment. The `TDefaultColumn` branch represents the first + * column in a comment. This column is not precedeeded by a name. `TNamedColumn(name)` represents a + * column containing expected results preceeded by the string `name:`. */ -private string expectationPattern() { - result = "(?:(f(?:\\+|-)):)?((?:[A-Za-z-_]+)(?:\\s*,\\s*[A-Za-z-_]+)*)(?:=(.*))?" +private newtype TColumn = + TDefaultColumn() or + TNamedColumn(string name) { name = ["MISSING", "SPURIOUS"] } + +bindingset[start, content] +private int getEndOfColumnPosition(int start, string content) { + result = + min(string name, int cand | + exists(TNamedColumn(name)) and + cand = content.indexOf(name + ":") and + cand > start + | + cand + ) + or + not exists(string name | + exists(TNamedColumn(name)) and + content.indexOf(name + ":") > start + ) and + result = content.length() +} + +private string getAnExpectation(LineComment comment, TColumn column) { + exists(string content | + content = comment.getContents().regexpCapture(expectationCommentPattern(), 1) and + ( + column = TDefaultColumn() and + exists(int end | + end = getEndOfColumnPosition(0, content) and + result = content.prefix(end).splitAt(" ").trim() + ) + or + exists(string name, int start, int end | + column = TNamedColumn(name) and + start = content.indexOf(name + ":") + name.length() + 1 and + end = getEndOfColumnPosition(start, content) and + result = content.substring(start, end).splitAt(" ").trim() + ) + ) and + result != "" + ) +} + +bindingset[expectation] +private string getATag(string expectation) { + ( + result = expectation.prefix(expectation.indexOf("=")).splitAt(",").trim() + or + not exists(expectation.indexOf("=")) and + result = expectation.splitAt(",").trim() + ) } -private string getAnExpectation(LineComment comment) { - result = comment.getContents().regexpCapture(expectationCommentPattern(), 1).splitAt("$").trim() and - result != "" +bindingset[expectation] +private string getValueForTag(string expectation) { + result = expectation.suffix(expectation.indexOf("=") + 1) +} + +private string getColumnString(TColumn column) { + column = TDefaultColumn() and result = "" + or + column = TNamedColumn(result) +} + +/** + * RegEx pattern to match a single expected result, not including the leading `$`. It consists of one or + * more comma-separated tags containing only letters, `-`, and `_`, optionally followed by `=` and the + * expected value. + */ +private string expectationPattern() { + result = "((?:[A-Za-z-_]+)(?:\\s*,\\s*[A-Za-z-_]+)*)(?:=(.*))?" } private newtype TFailureLocatable = @@ -183,24 +250,19 @@ private newtype TFailureLocatable = test.hasActualResult(location, element, tag, value) } or TValidExpectation(LineComment comment, string tag, string value, string knownFailure) { - exists(string expectation | - expectation = getAnExpectation(comment) and - expectation.regexpMatch(expectationPattern()) and - tag = expectation.regexpCapture(expectationPattern(), 2).splitAt(",").trim() and + exists(string expectation, TColumn column | + expectation = getAnExpectation(comment, column) and + tag = getATag(expectation) and ( - if exists(expectation.regexpCapture(expectationPattern(), 3)) - then value = expectation.regexpCapture(expectationPattern(), 3) + if exists(getValueForTag(expectation)) + then value = getValueForTag(expectation) else value = "" ) and - ( - if exists(expectation.regexpCapture(expectationPattern(), 1)) - then knownFailure = expectation.regexpCapture(expectationPattern(), 1) - else knownFailure = "" - ) + knownFailure = getColumnString(column) ) } or TInvalidExpectation(LineComment comment, string expectation) { - expectation = getAnExpectation(comment) and + expectation = getAnExpectation(comment, _) and not expectation.regexpMatch(expectationPattern()) } @@ -265,16 +327,17 @@ private class ValidExpectation extends Expectation, TValidExpectation { } } +/* Note: These next three classes correspond to all the possible values of type `TColumn`. */ class GoodExpectation extends ValidExpectation { GoodExpectation() { getKnownFailure() = "" } } class FalsePositiveExpectation extends ValidExpectation { - FalsePositiveExpectation() { getKnownFailure() = "f+" } + FalsePositiveExpectation() { getKnownFailure() = "SPURIOUS" } } class FalseNegativeExpectation extends ValidExpectation { - FalseNegativeExpectation() { getKnownFailure() = "f-" } + FalseNegativeExpectation() { getKnownFailure() = "MISSING" } } class InvalidExpectation extends Expectation, TInvalidExpectation { From b5234f92451bf66a73d04f139acac08f8f705ea7 Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Thu, 29 Oct 2020 19:11:54 +0100 Subject: [PATCH 05/12] C++: Update IR inline-expectation tests --- .../library-tests/ir/points_to/points_to.cpp | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/cpp/ql/test/library-tests/ir/points_to/points_to.cpp b/cpp/ql/test/library-tests/ir/points_to/points_to.cpp index 249f08352abd..024a3190706d 100644 --- a/cpp/ql/test/library-tests/ir/points_to/points_to.cpp +++ b/cpp/ql/test/library-tests/ir/points_to/points_to.cpp @@ -38,27 +38,27 @@ void Locals() { void PointsTo( int a, //$raw,ussa=a - Point& b, //$raw,ussa=b $ussa=*b - Point* c, //$raw,ussa=c $ussa=*c - int* d, //$raw,ussa=d $ussa=*d - DerivedSI* e, //$raw,ussa=e $ussa=*e - DerivedMI* f, //$raw,ussa=f $ussa=*f - DerivedVI* g //$raw,ussa=g $ussa=*g + Point& b, //$raw,ussa=b ussa=*b + Point* c, //$raw,ussa=c ussa=*c + int* d, //$raw,ussa=d ussa=*d + DerivedSI* e, //$raw,ussa=e ussa=*e + DerivedMI* f, //$raw,ussa=f ussa=*f + DerivedVI* g //$raw,ussa=g ussa=*g ) { int i = a; //$raw,ussa=a i = *&a; //$raw,ussa=a i = *(&a + 0); //$raw,ussa=a - i = b.x; //$raw,ussa=b $ussa=*b[0..4) - i = b.y; //$raw,ussa=b $ussa=*b[4..8) - i = c->x; //$raw,ussa=c $ussa=*c[0..4) - i = c->y; //$raw,ussa=c $ussa=*c[4..8) - i = *d; //$raw,ussa=d $ussa=*d[0..4) - i = *(d + 0); //$raw,ussa=d $ussa=*d[0..4) - i = d[5]; //$raw,ussa=d $ussa=*d[20..24) - i = 5[d]; //$raw,ussa=d $ussa=*d[20..24) - i = d[a]; //$raw,ussa=d $raw,ussa=a $ussa=*d[?..?) - i = a[d]; //$raw,ussa=d $raw,ussa=a $ussa=*d[?..?) + i = b.x; //$raw,ussa=b ussa=*b[0..4) + i = b.y; //$raw,ussa=b ussa=*b[4..8) + i = c->x; //$raw,ussa=c ussa=*c[0..4) + i = c->y; //$raw,ussa=c ussa=*c[4..8) + i = *d; //$raw,ussa=d ussa=*d[0..4) + i = *(d + 0); //$raw,ussa=d ussa=*d[0..4) + i = d[5]; //$raw,ussa=d ussa=*d[20..24) + i = 5[d]; //$raw,ussa=d ussa=*d[20..24) + i = d[a]; //$raw,ussa=d raw,ussa=a ussa=*d[?..?) + i = a[d]; //$raw,ussa=d raw,ussa=a ussa=*d[?..?) int* p = &b.x; //$raw,ussa=b i = *p; //$ussa=*b[0..4) @@ -70,18 +70,18 @@ void PointsTo( i = *p; //$ussa=*c[4..8) p = &d[5]; //$raw,ussa=d i = *p; //$ussa=*d[20..24) - p = &d[a]; //$raw,ussa=d $raw,ussa=a + p = &d[a]; //$raw,ussa=d raw,ussa=a i = *p; //$ussa=*d[?..?) - Point* q = &c[a]; //$raw,ussa=c $raw,ussa=a + Point* q = &c[a]; //$raw,ussa=c raw,ussa=a i = q->x; //$ussa=*c[?..?) i = q->y; //$ussa=*c[?..?) - i = e->b1; //$raw,ussa=e $ussa=*e[0..4) - i = e->dsi; //$raw,ussa=e $ussa=*e[4..8) - i = f->b1; //$raw,ussa=f $ussa=*f[0..4) - i = f->b2; //$raw,ussa=f $ussa=*f[4..8) - i = f->dmi; //$raw,ussa=f $ussa=*f[8..12) - i = g->b1; //$raw,ussa=g $ussa=*g[?..?) - i = g->dvi; //$raw,ussa=g $ussa=*g[8..12) + i = e->b1; //$raw,ussa=e ussa=*e[0..4) + i = e->dsi; //$raw,ussa=e ussa=*e[4..8) + i = f->b1; //$raw,ussa=f ussa=*f[0..4) + i = f->b2; //$raw,ussa=f ussa=*f[4..8) + i = f->dmi; //$raw,ussa=f ussa=*f[8..12) + i = g->b1; //$raw,ussa=g ussa=*g[?..?) + i = g->dvi; //$raw,ussa=g ussa=*g[8..12) } \ No newline at end of file From ee77e988b29bc54aa2b49ddb963423bcdde48763 Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Fri, 30 Oct 2020 14:03:19 +0100 Subject: [PATCH 06/12] C++: Allow strings in inline-expectation tests --- .../TestUtilities/InlineExpectationsTest.qll | 56 ++++++++----------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/cpp/ql/test/TestUtilities/InlineExpectationsTest.qll b/cpp/ql/test/TestUtilities/InlineExpectationsTest.qll index ad50cc11a8cd..d5eebe733bbc 100644 --- a/cpp/ql/test/TestUtilities/InlineExpectationsTest.qll +++ b/cpp/ql/test/TestUtilities/InlineExpectationsTest.qll @@ -192,40 +192,30 @@ private int getEndOfColumnPosition(int start, string content) { result = content.length() } -private string getAnExpectation(LineComment comment, TColumn column) { +private predicate getAnExpectation( + LineComment comment, TColumn column, string expectation, string tags, string value +) { exists(string content | content = comment.getContents().regexpCapture(expectationCommentPattern(), 1) and ( column = TDefaultColumn() and exists(int end | end = getEndOfColumnPosition(0, content) and - result = content.prefix(end).splitAt(" ").trim() + expectation = content.prefix(end).regexpFind(expectationPattern(), _, _).trim() ) or exists(string name, int start, int end | column = TNamedColumn(name) and start = content.indexOf(name + ":") + name.length() + 1 and end = getEndOfColumnPosition(start, content) and - result = content.substring(start, end).splitAt(" ").trim() + expectation = content.substring(start, end).regexpFind(expectationPattern(), _, _).trim() ) - ) and - result != "" - ) -} - -bindingset[expectation] -private string getATag(string expectation) { - ( - result = expectation.prefix(expectation.indexOf("=")).splitAt(",").trim() - or - not exists(expectation.indexOf("=")) and - result = expectation.splitAt(",").trim() - ) -} - -bindingset[expectation] -private string getValueForTag(string expectation) { - result = expectation.suffix(expectation.indexOf("=") + 1) + ) + ) and + tags = expectation.regexpCapture(expectationPattern(), 1) and + if exists(expectation.regexpCapture(expectationPattern(), 2)) + then value = expectation.regexpCapture(expectationPattern(), 2) + else value = "" } private string getColumnString(TColumn column) { @@ -236,11 +226,16 @@ private string getColumnString(TColumn column) { /** * RegEx pattern to match a single expected result, not including the leading `$`. It consists of one or - * more comma-separated tags containing only letters, `-`, and `_`, optionally followed by `=` and the - * expected value. + * more comma-separated tags containing only letters, digits, `-` and `_` (note that the first character + * must not be a digit), optionally followed by `=` and the expected value. */ private string expectationPattern() { - result = "((?:[A-Za-z-_]+)(?:\\s*,\\s*[A-Za-z-_]+)*)(?:=(.*))?" + exists(string tag, string tags, string value | + tag = "[A-Za-z-_][A-Za-z-_0-9]*" and + tags = "((?:" + tag + ")(?:\\s*,\\s*" + tag + ")*)" and + value = "((?:\"[^\"]*\"|\\S+)*)" and + result = tags + "(?:=" + value + ")?" + ) } private newtype TFailureLocatable = @@ -250,19 +245,14 @@ private newtype TFailureLocatable = test.hasActualResult(location, element, tag, value) } or TValidExpectation(LineComment comment, string tag, string value, string knownFailure) { - exists(string expectation, TColumn column | - expectation = getAnExpectation(comment, column) and - tag = getATag(expectation) and - ( - if exists(getValueForTag(expectation)) - then value = getValueForTag(expectation) - else value = "" - ) and + exists(TColumn column, string tags | + getAnExpectation(comment, column, _, tags, value) and + tag = tags.splitAt(",") and knownFailure = getColumnString(column) ) } or TInvalidExpectation(LineComment comment, string expectation) { - expectation = getAnExpectation(comment, _) and + getAnExpectation(comment, _, expectation, _, _) and not expectation.regexpMatch(expectationPattern()) } From 6ac740a490e554e76a89dbb4dba9f5ae059626da Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Fri, 30 Oct 2020 14:03:30 +0100 Subject: [PATCH 07/12] Python: Sync identical file --- .../TestUtilities/InlineExpectationsTest.qll | 56 ++++++++----------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/python/ql/test/TestUtilities/InlineExpectationsTest.qll b/python/ql/test/TestUtilities/InlineExpectationsTest.qll index ad50cc11a8cd..d5eebe733bbc 100644 --- a/python/ql/test/TestUtilities/InlineExpectationsTest.qll +++ b/python/ql/test/TestUtilities/InlineExpectationsTest.qll @@ -192,40 +192,30 @@ private int getEndOfColumnPosition(int start, string content) { result = content.length() } -private string getAnExpectation(LineComment comment, TColumn column) { +private predicate getAnExpectation( + LineComment comment, TColumn column, string expectation, string tags, string value +) { exists(string content | content = comment.getContents().regexpCapture(expectationCommentPattern(), 1) and ( column = TDefaultColumn() and exists(int end | end = getEndOfColumnPosition(0, content) and - result = content.prefix(end).splitAt(" ").trim() + expectation = content.prefix(end).regexpFind(expectationPattern(), _, _).trim() ) or exists(string name, int start, int end | column = TNamedColumn(name) and start = content.indexOf(name + ":") + name.length() + 1 and end = getEndOfColumnPosition(start, content) and - result = content.substring(start, end).splitAt(" ").trim() + expectation = content.substring(start, end).regexpFind(expectationPattern(), _, _).trim() ) - ) and - result != "" - ) -} - -bindingset[expectation] -private string getATag(string expectation) { - ( - result = expectation.prefix(expectation.indexOf("=")).splitAt(",").trim() - or - not exists(expectation.indexOf("=")) and - result = expectation.splitAt(",").trim() - ) -} - -bindingset[expectation] -private string getValueForTag(string expectation) { - result = expectation.suffix(expectation.indexOf("=") + 1) + ) + ) and + tags = expectation.regexpCapture(expectationPattern(), 1) and + if exists(expectation.regexpCapture(expectationPattern(), 2)) + then value = expectation.regexpCapture(expectationPattern(), 2) + else value = "" } private string getColumnString(TColumn column) { @@ -236,11 +226,16 @@ private string getColumnString(TColumn column) { /** * RegEx pattern to match a single expected result, not including the leading `$`. It consists of one or - * more comma-separated tags containing only letters, `-`, and `_`, optionally followed by `=` and the - * expected value. + * more comma-separated tags containing only letters, digits, `-` and `_` (note that the first character + * must not be a digit), optionally followed by `=` and the expected value. */ private string expectationPattern() { - result = "((?:[A-Za-z-_]+)(?:\\s*,\\s*[A-Za-z-_]+)*)(?:=(.*))?" + exists(string tag, string tags, string value | + tag = "[A-Za-z-_][A-Za-z-_0-9]*" and + tags = "((?:" + tag + ")(?:\\s*,\\s*" + tag + ")*)" and + value = "((?:\"[^\"]*\"|\\S+)*)" and + result = tags + "(?:=" + value + ")?" + ) } private newtype TFailureLocatable = @@ -250,19 +245,14 @@ private newtype TFailureLocatable = test.hasActualResult(location, element, tag, value) } or TValidExpectation(LineComment comment, string tag, string value, string knownFailure) { - exists(string expectation, TColumn column | - expectation = getAnExpectation(comment, column) and - tag = getATag(expectation) and - ( - if exists(getValueForTag(expectation)) - then value = getValueForTag(expectation) - else value = "" - ) and + exists(TColumn column, string tags | + getAnExpectation(comment, column, _, tags, value) and + tag = tags.splitAt(",") and knownFailure = getColumnString(column) ) } or TInvalidExpectation(LineComment comment, string expectation) { - expectation = getAnExpectation(comment, _) and + getAnExpectation(comment, _, expectation, _, _) and not expectation.regexpMatch(expectationPattern()) } From 45b24a9bc835f566f3e75b146d68d5cdd047279a Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Fri, 30 Oct 2020 14:04:29 +0100 Subject: [PATCH 08/12] Python: Update inline-expectation tests --- .../experimental/dataflow/global-flow/test.py | 8 +-- .../dataflow/typetracking/attribute_tests.py | 72 +++++++++---------- .../dataflow/typetracking/test.py | 2 +- .../library-tests/frameworks/dill/Decoding.py | 2 +- .../frameworks/django-v1/routing_test.py | 12 ++-- .../frameworks/django-v2-v3/routing_test.py | 16 ++--- .../frameworks/django-v2-v3/taint_test.py | 2 +- .../frameworks/django/SqlExecution.py | 2 +- .../frameworks/flask/old_test.py | 10 +-- .../frameworks/flask/routing_test.py | 4 +- .../frameworks/flask/taint_test.py | 4 +- .../stdlib/CodeExecutionPossibleFP1.py | 2 +- .../stdlib/CodeExecutionPossibleFP2.py | 2 +- .../stdlib/CodeExecutionPossibleFP3.py | 2 +- .../frameworks/stdlib/Decoding.py | 4 +- .../stdlib/SystemCommandExecution.py | 34 ++++----- .../library-tests/frameworks/yaml/Decoding.py | 6 +- 17 files changed, 92 insertions(+), 92 deletions(-) diff --git a/python/ql/test/experimental/dataflow/global-flow/test.py b/python/ql/test/experimental/dataflow/global-flow/test.py index c8f34db211eb..ab2da1c2959d 100644 --- a/python/ql/test/experimental/dataflow/global-flow/test.py +++ b/python/ql/test/experimental/dataflow/global-flow/test.py @@ -6,7 +6,7 @@ # Multiple assignment -g1, g2 = [6], [7] # $writes=g1 $writes=g2 +g1, g2 = [6], [7] # $writes=g1 writes=g2 # Assignment that's only referenced in this scope. This one will not give rise to a `ModuleVariableNode`. @@ -22,7 +22,7 @@ # The following assignment should not be a `ModuleVariableNode`, # but currently our analysis thinks `g_mod` might be used in the `print` call -g_mod = [10] # $f+:writes=g_mod +g_mod = [10] # $ SPURIOUS: writes=g_mod print("foo") g_mod = [100] # $writes=g_mod @@ -81,10 +81,10 @@ def use_foo(): # Partial imports -from bar import baz_attr, quux_attr # $writes=baz_attr $writes=quux_attr +from bar import baz_attr, quux_attr # $writes=baz_attr writes=quux_attr def use_partial_import(): - print(baz_attr, quux_attr) # $reads=baz_attr $reads=quux_attr + print(baz_attr, quux_attr) # $reads=baz_attr reads=quux_attr # Aliased imports diff --git a/python/ql/test/experimental/dataflow/typetracking/attribute_tests.py b/python/ql/test/experimental/dataflow/typetracking/attribute_tests.py index 5e8e87f8ae32..af8fe9bd4e39 100644 --- a/python/ql/test/experimental/dataflow/typetracking/attribute_tests.py +++ b/python/ql/test/experimental/dataflow/typetracking/attribute_tests.py @@ -3,32 +3,32 @@ class SomeClass: def simple_read_write(): x = SomeClass() # $tracked=foo - x.foo = tracked # $tracked $tracked=foo - y = x.foo # $tracked=foo $tracked + x.foo = tracked # $tracked tracked=foo + y = x.foo # $tracked=foo tracked do_stuff(y) # $tracked def foo(): x = SomeClass() # $tracked=attr bar(x) # $tracked=attr - x.attr = tracked # $tracked=attr $tracked + x.attr = tracked # $tracked=attr tracked baz(x) # $tracked=attr def bar(x): # $tracked=attr - z = x.attr # $tracked $tracked=attr + z = x.attr # $tracked tracked=attr do_stuff(z) # $tracked -def expects_int(x): # $int=field $f+:str=field - do_int_stuff(x.field) # $int $f+:str $int=field $f+:str=field +def expects_int(x): # $int=field SPURIOUS: str=field + do_int_stuff(x.field) # $int int=field SPURIOUS: str str=field -def expects_string(x): # $f+:int=field $str=field - do_string_stuff(x.field) # $f+:int $str $f+:int=field $str=field +def expects_string(x): # $ str=field SPURIOUS: int=field + do_string_stuff(x.field) # $str str=field SPURIOUS: int int=field def test_incompatible_types(): x = SomeClass() # $int,str=field - x.field = int(5) # $int=field $f+:str=field $int $f+:str - expects_int(x) # $int=field $f+:str=field - x.field = str("Hello") # $f+:int=field $str=field $f+:int $str - expects_string(x) # $f+:int=field $str=field + x.field = int(5) # $int=field int SPURIOUS: str=field str + expects_int(x) # $int=field SPURIOUS: str=field + x.field = str("Hello") # $str=field str SPURIOUS: int=field int + expects_string(x) # $ str=field SPURIOUS: int=field # Attributes assigned statically to a class @@ -36,9 +36,9 @@ def test_incompatible_types(): class MyClass: # $tracked=field field = tracked # $tracked -lookup = MyClass.field # $tracked $tracked=field +lookup = MyClass.field # $tracked tracked=field instance = MyClass() # $tracked=field -lookup2 = instance.field # $f-:tracked +lookup2 = instance.field # MISSING: tracked ## Dynamic attribute access @@ -46,56 +46,56 @@ class MyClass: # $tracked=field def setattr_immediate_write(): x = SomeClass() # $tracked=foo - setattr(x,"foo", tracked) # $tracked $tracked=foo - y = x.foo # $tracked $tracked=foo + setattr(x,"foo", tracked) # $tracked tracked=foo + y = x.foo # $tracked tracked=foo do_stuff(y) # $tracked def getattr_immediate_read(): x = SomeClass() # $tracked=foo - x.foo = tracked # $tracked $tracked=foo - y = getattr(x,"foo") # $tracked $tracked=foo + x.foo = tracked # $tracked tracked=foo + y = getattr(x,"foo") # $tracked tracked=foo do_stuff(y) # $tracked def setattr_indirect_write(): attr = "foo" x = SomeClass() # $tracked=foo - setattr(x, attr, tracked) # $tracked $tracked=foo - y = x.foo # $tracked $tracked=foo + setattr(x, attr, tracked) # $tracked tracked=foo + y = x.foo # $tracked tracked=foo do_stuff(y) # $tracked def getattr_indirect_read(): attr = "foo" x = SomeClass() # $tracked=foo - x.foo = tracked # $tracked $tracked=foo - y = getattr(x, attr) #$tracked $tracked=foo + x.foo = tracked # $tracked tracked=foo + y = getattr(x, attr) #$tracked tracked=foo do_stuff(y) # $tracked # Via `__dict__` -- not currently implemented. def dunder_dict_immediate_write(): - x = SomeClass() # $f-:tracked=foo - x.__dict__["foo"] = tracked # $tracked $f-:tracked=foo - y = x.foo # $f-:tracked $f-:tracked=foo - do_stuff(y) # $f-:tracked + x = SomeClass() # $ MISSING: tracked=foo + x.__dict__["foo"] = tracked # $tracked MISSING: tracked=foo + y = x.foo # $ MISSING: tracked tracked=foo + do_stuff(y) # $ MISSING: tracked def dunder_dict_immediate_read(): x = SomeClass() # $tracked=foo - x.foo = tracked # $tracked $tracked=foo - y = x.__dict__["foo"] # $f-:tracked $tracked=foo - do_stuff(y) # $f-:tracked + x.foo = tracked # $tracked tracked=foo + y = x.__dict__["foo"] # $ tracked=foo MISSING: tracked + do_stuff(y) # $ MISSING: tracked def dunder_dict_indirect_write(): attr = "foo" - x = SomeClass() # $f-:tracked=foo - x.__dict__[attr] = tracked # $tracked $f-:tracked=foo - y = x.foo # $f-:tracked $f-:tracked=foo - do_stuff(y) # $f-:tracked + x = SomeClass() # $ MISSING: tracked=foo + x.__dict__[attr] = tracked # $tracked MISSING: tracked=foo + y = x.foo # $ MISSING: tracked tracked=foo + do_stuff(y) # $ MISSING: tracked def dunder_dict_indirect_read(): attr = "foo" x = SomeClass() # $tracked=foo - x.foo = tracked # $tracked $tracked=foo - y = x.__dict__[attr] # $f-:tracked $tracked=foo - do_stuff(y) # $f-:tracked + x.foo = tracked # $tracked tracked=foo + y = x.__dict__[attr] # $ tracked=foo MISSING: tracked + do_stuff(y) # $ MISSING: tracked diff --git a/python/ql/test/experimental/dataflow/typetracking/test.py b/python/ql/test/experimental/dataflow/typetracking/test.py index 6fbf4cfabb16..432518327f5a 100644 --- a/python/ql/test/experimental/dataflow/typetracking/test.py +++ b/python/ql/test/experimental/dataflow/typetracking/test.py @@ -27,7 +27,7 @@ def baz(): def id(x): # $tracked return x # $tracked -def use_tracked_quux(x): # $f-:tracked +def use_tracked_quux(x): # $ MISSING: tracked do_stuff(y) # call after return -- not tracked in here. def quux(): diff --git a/python/ql/test/experimental/library-tests/frameworks/dill/Decoding.py b/python/ql/test/experimental/library-tests/frameworks/dill/Decoding.py index 67d646e5ef7c..a02b013104b6 100644 --- a/python/ql/test/experimental/library-tests/frameworks/dill/Decoding.py +++ b/python/ql/test/experimental/library-tests/frameworks/dill/Decoding.py @@ -1,3 +1,3 @@ import dill -dill.loads(payload) # $decodeInput=payload $decodeOutput=Attribute() $decodeFormat=dill $decodeMayExecuteInput +dill.loads(payload) # $decodeInput=payload decodeOutput=Attribute() decodeFormat=dill decodeMayExecuteInput diff --git a/python/ql/test/experimental/library-tests/frameworks/django-v1/routing_test.py b/python/ql/test/experimental/library-tests/frameworks/django-v1/routing_test.py index f50ab8b35d1e..81fd440a6d2f 100644 --- a/python/ql/test/experimental/library-tests/frameworks/django-v1/routing_test.py +++ b/python/ql/test/experimental/library-tests/frameworks/django-v1/routing_test.py @@ -4,7 +4,7 @@ from django.views.generic import View -def url_match_xss(request, foo, bar, no_taint=None): # $routeHandler $routedParameter=foo $routedParameter=bar +def url_match_xss(request, foo, bar, no_taint=None): # $routeHandler routedParameter=foo routedParameter=bar return HttpResponse('url_match_xss: {} {}'.format(foo, bar)) @@ -26,22 +26,22 @@ class Foo(object): # Note: since Foo is used as the super type in a class view, it will be able to handle requests. - def post(self, request, untrusted): # $f-:routeHandler $f-:routedParameter=untrusted + def post(self, request, untrusted): # $ MISSING: routeHandler routedParameter=untrusted return HttpResponse('Foo post: {}'.format(untrusted)) class ClassView(View, Foo): - def get(self, request, untrusted): # $f-:routeHandler $f-:routedParameter=untrusted + def get(self, request, untrusted): # $ MISSING: routeHandler routedParameter=untrusted return HttpResponse('ClassView get: {}'.format(untrusted)) -def show_articles(request, page_number=1): # $routeHandler $routedParameter=page_number +def show_articles(request, page_number=1): # $routeHandler routedParameter=page_number page_number = int(page_number) return HttpResponse('articles page: {}'.format(page_number)) -def xxs_positional_arg(request, arg0, arg1, no_taint=None): # $routeHandler $routedParameter=arg0 $routedParameter=arg1 +def xxs_positional_arg(request, arg0, arg1, no_taint=None): # $routeHandler routedParameter=arg0 routedParameter=arg1 return HttpResponse('xxs_positional_arg: {} {}'.format(arg0, arg1)) @@ -62,7 +62,7 @@ def xxs_positional_arg(request, arg0, arg1, no_taint=None): # $routeHandler $ro ################################################################################ # Using patterns() for routing -def show_user(request, username): # $routeHandler $routedParameter=username +def show_user(request, username): # $routeHandler routedParameter=username return HttpResponse('show_user {}'.format(username)) diff --git a/python/ql/test/experimental/library-tests/frameworks/django-v2-v3/routing_test.py b/python/ql/test/experimental/library-tests/frameworks/django-v2-v3/routing_test.py index 5321e1872739..de5e1d738236 100644 --- a/python/ql/test/experimental/library-tests/frameworks/django-v2-v3/routing_test.py +++ b/python/ql/test/experimental/library-tests/frameworks/django-v2-v3/routing_test.py @@ -4,7 +4,7 @@ from django.views import View -def url_match_xss(request, foo, bar, no_taint=None): # $routeHandler $routedParameter=foo $routedParameter=bar +def url_match_xss(request, foo, bar, no_taint=None): # $routeHandler routedParameter=foo routedParameter=bar return HttpResponse('url_match_xss: {} {}'.format(foo, bar)) @@ -26,22 +26,22 @@ class Foo(object): # Note: since Foo is used as the super type in a class view, it will be able to handle requests. - def post(self, request, untrusted): # $f-:routeHandler $f-:routedParameter=untrusted + def post(self, request, untrusted): # $ MISSING: routeHandler routedParameter=untrusted return HttpResponse('Foo post: {}'.format(untrusted)) class ClassView(View, Foo): - def get(self, request, untrusted): # $f-:routeHandler $f-:routedParameter=untrusted + def get(self, request, untrusted): # $ MISSING: routeHandler routedParameter=untrusted return HttpResponse('ClassView get: {}'.format(untrusted)) -def show_articles(request, page_number=1): # $routeHandler $routedParameter=page_number +def show_articles(request, page_number=1): # $routeHandler routedParameter=page_number page_number = int(page_number) return HttpResponse('articles page: {}'.format(page_number)) -def xxs_positional_arg(request, arg0, arg1, no_taint=None): # $routeHandler $routedParameter=arg0 $routedParameter=arg1 +def xxs_positional_arg(request, arg0, arg1, no_taint=None): # $routeHandler routedParameter=arg0 routedParameter=arg1 return HttpResponse('xxs_positional_arg: {} {}'.format(arg0, arg1)) @@ -75,13 +75,13 @@ def re_path_kwargs(request): # $routeHandler ################################################################################ # saying page_number is an externally controlled *string* is a bit strange, when we have an int converter :O -def page_number(request, page_number=1): # $routeHandler $routedParameter=page_number +def page_number(request, page_number=1): # $routeHandler routedParameter=page_number return HttpResponse('page_number: {}'.format(page_number)) -def foo_bar_baz(request, foo, bar, baz): # $routeHandler $routedParameter=foo $routedParameter=bar $routedParameter=baz +def foo_bar_baz(request, foo, bar, baz): # $routeHandler routedParameter=foo routedParameter=bar routedParameter=baz return HttpResponse('foo_bar_baz: {} {} {}'.format(foo, bar, baz)) -def path_kwargs(request, foo, bar): # $routeHandler $routedParameter=foo $routedParameter=bar +def path_kwargs(request, foo, bar): # $routeHandler routedParameter=foo routedParameter=bar return HttpResponse('path_kwargs: {} {} {}'.format(foo, bar)) def not_valid_identifier(request): # $routeHandler diff --git a/python/ql/test/experimental/library-tests/frameworks/django-v2-v3/taint_test.py b/python/ql/test/experimental/library-tests/frameworks/django-v2-v3/taint_test.py index 6154038d28bc..6e0529f0fc33 100644 --- a/python/ql/test/experimental/library-tests/frameworks/django-v2-v3/taint_test.py +++ b/python/ql/test/experimental/library-tests/frameworks/django-v2-v3/taint_test.py @@ -3,7 +3,7 @@ from django.http import HttpRequest -def test_taint(request: HttpRequest, foo, bar, baz=None): # $routeHandler $routedParameter=foo $routedParameter=bar +def test_taint(request: HttpRequest, foo, bar, baz=None): # $routeHandler routedParameter=foo routedParameter=bar ensure_tainted(foo, bar) ensure_not_tainted(baz) diff --git a/python/ql/test/experimental/library-tests/frameworks/django/SqlExecution.py b/python/ql/test/experimental/library-tests/frameworks/django/SqlExecution.py index 4065307194c3..3ddc5d799fc8 100644 --- a/python/ql/test/experimental/library-tests/frameworks/django/SqlExecution.py +++ b/python/ql/test/experimental/library-tests/frameworks/django/SqlExecution.py @@ -23,7 +23,7 @@ def test_model(): User.objects.annotate(RawSQL("foo"), RawSQL("bar")) # $getSql="foo" $getSql="bar" User.objects.annotate(val=RawSQL("some sql")) # $getSql="some sql" User.objects.extra("some sql") # $getSql="some sql" - User.objects.extra(select="select", where="where", tables="tables", order_by="order_by") # $getSql="select" $getSql="where" $getSql="tables" $getSql="order_by" + User.objects.extra(select="select", where="where", tables="tables", order_by="order_by") # $getSql="select" getSql="where" getSql="tables" getSql="order_by" raw = RawSQL("so raw") User.objects.annotate(val=raw) # $getSql="so raw" diff --git a/python/ql/test/experimental/library-tests/frameworks/flask/old_test.py b/python/ql/test/experimental/library-tests/frameworks/flask/old_test.py index 1d199d31004f..c4687ce10abb 100644 --- a/python/ql/test/experimental/library-tests/frameworks/flask/old_test.py +++ b/python/ql/test/experimental/library-tests/frameworks/flask/old_test.py @@ -11,7 +11,7 @@ def hello_world(): # $routeHandler class MyView(MethodView): - def get(self, user_id): # $f-:routeHandler + def get(self, user_id): # $ MISSING: routeHandler if user_id is None: # return a list of users pass @@ -46,21 +46,21 @@ def safe(): # $routeHandler return make_response("Your name is " + escape(first_name)) @app.route("/hello/") # $routeSetup="/hello/" -def hello(name): # $routeHandler $routedParameter=name +def hello(name): # $routeHandler routedParameter=name return make_response("Your name is " + name) @app.route("/foo/") # $routeSetup="/foo/" -def foo(subpath): # $routeHandler $routedParameter=subpath +def foo(subpath): # $routeHandler routedParameter=subpath return make_response("The subpath is " + subpath) @app.route("/multiple/") # $routeSetup="/multiple/" @app.route("/multiple/foo/") # $routeSetup="/multiple/foo/" @app.route("/multiple/bar/") # $routeSetup="/multiple/bar/" -def multiple(foo=None, bar=None): # $routeHandler $routedParameter=foo $routedParameter=bar +def multiple(foo=None, bar=None): # $routeHandler routedParameter=foo routedParameter=bar return make_response("foo={!r} bar={!r}".format(foo, bar)) @app.route("/complex/") # $routeSetup="/complex/" -def complex(lang_code): # $routeHandler $routedParameter=lang_code +def complex(lang_code): # $routeHandler routedParameter=lang_code return make_response("lang_code {}".format(lang_code)) if __name__ == "__main__": diff --git a/python/ql/test/experimental/library-tests/frameworks/flask/routing_test.py b/python/ql/test/experimental/library-tests/frameworks/flask/routing_test.py index 92326d514608..643cc8c04b6a 100644 --- a/python/ql/test/experimental/library-tests/frameworks/flask/routing_test.py +++ b/python/ql/test/experimental/library-tests/frameworks/flask/routing_test.py @@ -16,14 +16,14 @@ def index(): # $routeHandler # We don't support this yet, and I think that's OK -def later_set(): # $f-:routeHandler +def later_set(): # $ MISSING: routeHandler return make_response("later_set") app.add_url_rule('/later-set', 'later_set', view_func=None) # $routeSetup="/later-set" app.view_functions['later_set'] = later_set @app.route(UNKNOWN_ROUTE) # $routeSetup -def unkown_route(foo, bar): # $routeHandler $routedParameter=foo $routedParameter=bar +def unkown_route(foo, bar): # $routeHandler routedParameter=foo routedParameter=bar return make_response("unkown_route") diff --git a/python/ql/test/experimental/library-tests/frameworks/flask/taint_test.py b/python/ql/test/experimental/library-tests/frameworks/flask/taint_test.py index 267d0e507608..fd2865b5df3b 100644 --- a/python/ql/test/experimental/library-tests/frameworks/flask/taint_test.py +++ b/python/ql/test/experimental/library-tests/frameworks/flask/taint_test.py @@ -2,7 +2,7 @@ app = Flask(__name__) @app.route("/test_taint//") # $routeSetup="/test_taint//" -def test_taint(name = "World!", number="0", foo="foo"): # $routeHandler $routedParameter=name $routedParameter=number +def test_taint(name = "World!", number="0", foo="foo"): # $routeHandler routedParameter=name routedParameter=number ensure_tainted(name, number) ensure_not_tainted(foo) @@ -192,7 +192,7 @@ def test_taint(name = "World!", number="0", foo="foo"): # $routeHandler $routed @app.route("/debug//", methods=['GET']) # $routeSetup="/debug//" -def debug(foo, bar): # $routeHandler $routedParameter=foo $routedParameter=bar +def debug(foo, bar): # $routeHandler routedParameter=foo routedParameter=bar print("request.view_args", request.view_args) print("request.headers {!r}".format(request.headers)) diff --git a/python/ql/test/experimental/library-tests/frameworks/stdlib/CodeExecutionPossibleFP1.py b/python/ql/test/experimental/library-tests/frameworks/stdlib/CodeExecutionPossibleFP1.py index 4fc903c51263..047882c6a410 100644 --- a/python/ql/test/experimental/library-tests/frameworks/stdlib/CodeExecutionPossibleFP1.py +++ b/python/ql/test/experimental/library-tests/frameworks/stdlib/CodeExecutionPossibleFP1.py @@ -8,4 +8,4 @@ def eval(*args, **kwargs): # This function call might be marked as a code execution, but it actually isn't. -eval("print(42)") # $f+:getCode="print(42)" +eval("print(42)") # $ SPURIOUS: getCode="print(42)" diff --git a/python/ql/test/experimental/library-tests/frameworks/stdlib/CodeExecutionPossibleFP2.py b/python/ql/test/experimental/library-tests/frameworks/stdlib/CodeExecutionPossibleFP2.py index 9b1996734545..c67b0d9bc4af 100644 --- a/python/ql/test/experimental/library-tests/frameworks/stdlib/CodeExecutionPossibleFP2.py +++ b/python/ql/test/experimental/library-tests/frameworks/stdlib/CodeExecutionPossibleFP2.py @@ -10,4 +10,4 @@ def foo(*args, **kwargs): eval = foo # This function call might be marked as a code execution, but it actually isn't. -eval("print(42)") # $f+:getCode="print(42)" +eval("print(42)") # $ SPURIOUS: getCode="print(42)" diff --git a/python/ql/test/experimental/library-tests/frameworks/stdlib/CodeExecutionPossibleFP3.py b/python/ql/test/experimental/library-tests/frameworks/stdlib/CodeExecutionPossibleFP3.py index 280987d6ae42..7ec639b7315b 100644 --- a/python/ql/test/experimental/library-tests/frameworks/stdlib/CodeExecutionPossibleFP3.py +++ b/python/ql/test/experimental/library-tests/frameworks/stdlib/CodeExecutionPossibleFP3.py @@ -16,4 +16,4 @@ def foo(*args, **kwargs): builtins.eval = foo # This function call might be marked as a code execution, but it actually isn't. -eval("print(42)") # $f+:getCode="print(42)" +eval("print(42)") # $ SPURIOUS: getCode="print(42)" diff --git a/python/ql/test/experimental/library-tests/frameworks/stdlib/Decoding.py b/python/ql/test/experimental/library-tests/frameworks/stdlib/Decoding.py index 25bdd8bcc6cd..a84ccc28dd44 100644 --- a/python/ql/test/experimental/library-tests/frameworks/stdlib/Decoding.py +++ b/python/ql/test/experimental/library-tests/frameworks/stdlib/Decoding.py @@ -1,5 +1,5 @@ import pickle import marshal -pickle.loads(payload) # $decodeInput=payload $decodeOutput=Attribute() $decodeFormat=pickle $decodeMayExecuteInput -marshal.loads(payload) # $decodeInput=payload $decodeOutput=Attribute() $decodeFormat=marshal $decodeMayExecuteInput +pickle.loads(payload) # $decodeInput=payload decodeOutput=Attribute() decodeFormat=pickle decodeMayExecuteInput +marshal.loads(payload) # $decodeInput=payload decodeOutput=Attribute() decodeFormat=marshal decodeMayExecuteInput diff --git a/python/ql/test/experimental/library-tests/frameworks/stdlib/SystemCommandExecution.py b/python/ql/test/experimental/library-tests/frameworks/stdlib/SystemCommandExecution.py index ad4223a8c4b7..b56f1634ced2 100644 --- a/python/ql/test/experimental/library-tests/frameworks/stdlib/SystemCommandExecution.py +++ b/python/ql/test/experimental/library-tests/frameworks/stdlib/SystemCommandExecution.py @@ -69,7 +69,7 @@ def os_members(): subprocess.Popen("cmd1; cmd2", shell=True) # $getCommand="cmd1; cmd2" subprocess.Popen("cmd1; cmd2", shell="truthy string") # $getCommand="cmd1; cmd2" subprocess.Popen(["cmd1; cmd2", "shell-arg"], shell=True) # $getCommand="cmd1; cmd2" -subprocess.Popen("cmd1; cmd2", shell=True, executable="/bin/bash") # $getCommand="cmd1; cmd2" $getCommand="/bin/bash" +subprocess.Popen("cmd1; cmd2", shell=True, executable="/bin/bash") # $getCommand="cmd1; cmd2" getCommand="/bin/bash" subprocess.Popen("executable") # $getCommand="executable" subprocess.Popen(["executable", "arg0"]) # $getCommand="executable" @@ -86,30 +86,30 @@ def os_members(): ######################################## # actively using known shell as the executable -subprocess.Popen(["/bin/sh", "-c", "vuln"]) # $getCommand="/bin/sh" $f-:getCommand="vuln" -subprocess.Popen(["/bin/bash", "-c", "vuln"]) # $getCommand="/bin/bash" $f-:getCommand="vuln" -subprocess.Popen(["/bin/dash", "-c", "vuln"]) # $getCommand="/bin/dash" $f-:getCommand="vuln" -subprocess.Popen(["/bin/zsh", "-c", "vuln"]) # $getCommand="/bin/zsh" $f-:getCommand="vuln" +subprocess.Popen(["/bin/sh", "-c", "vuln"]) # $getCommand="/bin/sh" MISSING: getCommand="vuln" +subprocess.Popen(["/bin/bash", "-c", "vuln"]) # $getCommand="/bin/bash" MISSING: getCommand="vuln" +subprocess.Popen(["/bin/dash", "-c", "vuln"]) # $getCommand="/bin/dash" MISSING: getCommand="vuln" +subprocess.Popen(["/bin/zsh", "-c", "vuln"]) # $getCommand="/bin/zsh" MISSING: getCommand="vuln" -subprocess.Popen(["sh", "-c", "vuln"]) # $getCommand="sh" $f-:getCommand="vuln" -subprocess.Popen(["bash", "-c", "vuln"]) # $getCommand="bash" $f-:getCommand="vuln" -subprocess.Popen(["dash", "-c", "vuln"]) # $getCommand="dash" $f-:getCommand="vuln" -subprocess.Popen(["zsh", "-c", "vuln"]) # $getCommand="zsh" $f-:getCommand="vuln" +subprocess.Popen(["sh", "-c", "vuln"]) # $getCommand="sh" MISSING: getCommand="vuln" +subprocess.Popen(["bash", "-c", "vuln"]) # $getCommand="bash" MISSING: getCommand="vuln" +subprocess.Popen(["dash", "-c", "vuln"]) # $getCommand="dash" MISSING: getCommand="vuln" +subprocess.Popen(["zsh", "-c", "vuln"]) # $getCommand="zsh" MISSING: getCommand="vuln" # Check that we don't consider ANY argument a command injection sink subprocess.Popen(["sh", "/bin/python"]) # $getCommand="sh" -subprocess.Popen(["cmd.exe", "/c", "vuln"]) # $getCommand="cmd.exe" $f-:getCommand="vuln" -subprocess.Popen(["cmd.exe", "/C", "vuln"]) # $getCommand="cmd.exe" $f-:getCommand="vuln" -subprocess.Popen(["cmd", "/c", "vuln"]) # $getCommand="cmd" $f-:getCommand="vuln" -subprocess.Popen(["cmd", "/C", "vuln"]) # $getCommand="cmd" $f-:getCommand="vuln" +subprocess.Popen(["cmd.exe", "/c", "vuln"]) # $getCommand="cmd.exe" MISSING: getCommand="vuln" +subprocess.Popen(["cmd.exe", "/C", "vuln"]) # $getCommand="cmd.exe" MISSING: getCommand="vuln" +subprocess.Popen(["cmd", "/c", "vuln"]) # $getCommand="cmd" MISSING: getCommand="vuln" +subprocess.Popen(["cmd", "/C", "vuln"]) # $getCommand="cmd" MISSING: getCommand="vuln" -subprocess.Popen(["", "-c", "vuln"], executable="/bin/bash") # $getCommand="/bin/bash" $f-:getCommand="vuln" +subprocess.Popen(["", "-c", "vuln"], executable="/bin/bash") # $getCommand="/bin/bash" MISSING: getCommand="vuln" if UNKNOWN: - os.execl("/bin/sh", "", "-c", "vuln") # $getCommand="/bin/sh" $f-:getCommand="vuln" + os.execl("/bin/sh", "", "-c", "vuln") # $getCommand="/bin/sh" MISSING: getCommand="vuln" -os.spawnl(os.P_WAIT, "/bin/sh", "", "-c", "vuln") # $getCommand="/bin/sh" $f-:getCommand="vuln" +os.spawnl(os.P_WAIT, "/bin/sh", "", "-c", "vuln") # $getCommand="/bin/sh" MISSING: getCommand="vuln" ######################################## @@ -121,7 +121,7 @@ def os_members(): args = "" use_shell = False exe = "executable" -subprocess.Popen(args, shell=use_shell, executable=exe) # $f+:getCommand=args $getCommand=exe +subprocess.Popen(args, shell=use_shell, executable=exe) # $getCommand=exe SPURIOUS: getCommand=args ################################################################################ diff --git a/python/ql/test/experimental/library-tests/frameworks/yaml/Decoding.py b/python/ql/test/experimental/library-tests/frameworks/yaml/Decoding.py index 6bf3fbbd8eb2..6fa904f60779 100644 --- a/python/ql/test/experimental/library-tests/frameworks/yaml/Decoding.py +++ b/python/ql/test/experimental/library-tests/frameworks/yaml/Decoding.py @@ -1,6 +1,6 @@ import yaml from yaml import SafeLoader -yaml.load(payload) # $decodeInput=payload $decodeOutput=Attribute() $decodeFormat=YAML $decodeMayExecuteInput -yaml.load(payload, Loader=SafeLoader) # $decodeInput=payload $decodeOutput=Attribute() $decodeFormat=YAML -yaml.load(payload, Loader=yaml.BaseLoader) # $decodeInput=payload $decodeOutput=Attribute() $decodeFormat=YAML +yaml.load(payload) # $decodeInput=payload decodeOutput=Attribute() decodeFormat=YAML decodeMayExecuteInput +yaml.load(payload, Loader=SafeLoader) # $decodeInput=payload decodeOutput=Attribute() decodeFormat=YAML +yaml.load(payload, Loader=yaml.BaseLoader) # $decodeInput=payload decodeOutput=Attribute() decodeFormat=YAML From 0bc4d52d66e176ba77c5edf23d9767e9b86f588e Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Sat, 31 Oct 2020 17:40:19 +0100 Subject: [PATCH 09/12] Python: Update more tests annotations. It looks like we need to allow single-quote strings to support the existing Python use-cases, but let's do that in the next commit. --- .../django-v1/ConceptsTest.expected | 10 +++ .../frameworks/django-v1/response_test.py | 24 +++--- .../frameworks/flask/old_test.py | 2 +- .../frameworks/flask/response_test.py | 84 +++++++++---------- .../frameworks/stdlib/FileSystemAccess.py | 4 +- .../frameworks/stdlib/SafeAccessCheck.py | 4 +- 6 files changed, 69 insertions(+), 59 deletions(-) diff --git a/python/ql/test/experimental/library-tests/frameworks/django-v1/ConceptsTest.expected b/python/ql/test/experimental/library-tests/frameworks/django-v1/ConceptsTest.expected index e69de29bb2d1..78280a935cb5 100644 --- a/python/ql/test/experimental/library-tests/frameworks/django-v1/ConceptsTest.expected +++ b/python/ql/test/experimental/library-tests/frameworks/django-v1/ConceptsTest.expected @@ -0,0 +1,10 @@ +| response_test.py:16:12:16:86 | ControlFlowNode for HttpResponse() | Unexpected result: responseBody='' | +| response_test.py:16:89:16:171 | Comment # $HttpResponse mimetype=text/plain responseBody='' | Missing result:responseBody='', content_type="text/plain") # $HttpResponse $mimetype=text/plain $responseBody='' + return HttpResponse('', content_type="text/plain") # $HttpResponse mimetype=text/plain responseBody='' # XSS FP reported in https://github.com/github/codeql/issues/3466 # Note: This should be an open-redirect sink, but not an XSS sink. def or__redirect(request): - return HttpResponseRedirect(request.GET.get("next")) # $HttpResponse $mimetype=text/html; charset=utf-8 $responseBody=Attribute() + return HttpResponseRedirect(request.GET.get("next")) # $HttpResponse mimetype=text/html; charset=utf-8 responseBody=Attribute() # Ensure that simple subclasses are still vuln to XSS def xss__not_found(request): - return HttpResponseNotFound(request.GET.get("name")) # $HttpResponse $mimetype=text/html; charset=utf-8 $responseBody=Attribute() + return HttpResponseNotFound(request.GET.get("name")) # $HttpResponse mimetype=text/html; charset=utf-8 responseBody=Attribute() # Ensure we still have an XSS sink when manually setting the content_type to HTML def xss__manual_response_type(request): - return HttpResponse(request.GET.get("name"), content_type="text/html; charset=utf-8") # $HttpResponse $mimetype=text/html $responseBody=Attribute() + return HttpResponse(request.GET.get("name"), content_type="text/html; charset=utf-8") # $HttpResponse mimetype=text/html responseBody=Attribute() def xss__write(request): - response = HttpResponse() # $HttpResponse $mimetype=text/html; charset=utf-8 - response.write(request.GET.get("name")) # $HttpResponse $mimetype=text/html; charset=utf-8 $responseBody=Attribute() + response = HttpResponse() # $HttpResponse mimetype=text/html; charset=utf-8 + response.write(request.GET.get("name")) # $HttpResponse mimetype=text/html; charset=utf-8 responseBody=Attribute() # This is safe but probably a bug if the argument to `write` is not a result of `json.dumps` or similar. def safe__write_json(request): - response = JsonResponse() # $HttpResponse $mimetype=application/json - response.write(request.GET.get("name")) # $HttpResponse $mimetype=application/json $responseBody=Attribute() + response = JsonResponse() # $HttpResponse mimetype=application/json + response.write(request.GET.get("name")) # $HttpResponse mimetype=application/json responseBody=Attribute() # Ensure manual subclasses are vulnerable class CustomResponse(HttpResponse): @@ -43,11 +43,11 @@ def __init__(self, banner, content, *args, **kwargs): super().__init__(content, *args, content_type="text/html", **kwargs) def xss__custom_response(request): - return CustomResponse("ACME Responses", request.GET("name")) # $HttpResponse $f-:mimetype=text/html $f-:responseBody=Attribute() $f+:responseBody="ACME Responses" + return CustomResponse("ACME Responses", request.GET("name")) # $HttpResponse MISSING: mimetype=text/html responseBody=Attribute() SPURIOUS: responseBody="ACME Responses" class CustomJsonResponse(JsonResponse): def __init__(self, banner, content, *args, **kwargs): super().__init__(content, *args, content_type="text/html", **kwargs) def safe__custom_json_response(request): - return CustomJsonResponse("ACME Responses", {"foo": request.GET.get("foo")}) # $HttpResponse $mimetype=application/json $f-:responseBody=Dict $f+:responseBody="ACME Responses" \ No newline at end of file + return CustomJsonResponse("ACME Responses", {"foo": request.GET.get("foo")}) # $HttpResponse mimetype=application/json MISSING: responseBody=Dict SPURIOUS: responseBody="ACME Responses" \ No newline at end of file diff --git a/python/ql/test/experimental/library-tests/frameworks/flask/old_test.py b/python/ql/test/experimental/library-tests/frameworks/flask/old_test.py index 392cc3d064f4..5609e80006fa 100644 --- a/python/ql/test/experimental/library-tests/frameworks/flask/old_test.py +++ b/python/ql/test/experimental/library-tests/frameworks/flask/old_test.py @@ -33,7 +33,7 @@ def dangerous2(): # $routeHandler x = request.form['param0'] if request.method == "POST": return request.form['param1'] # $HttpResponse - return None # $f+:HttpResponse + return None # $ SPURIOUS: HttpResponse @app.route("/unsafe") # $routeSetup="/unsafe" def unsafe(): # $routeHandler diff --git a/python/ql/test/experimental/library-tests/frameworks/flask/response_test.py b/python/ql/test/experimental/library-tests/frameworks/flask/response_test.py index 29250656b82b..ce57b5f01f0d 100644 --- a/python/ql/test/experimental/library-tests/frameworks/flask/response_test.py +++ b/python/ql/test/experimental/library-tests/frameworks/flask/response_test.py @@ -7,7 +7,7 @@ @app.route("/html1") # $routeSetup="/html1" def html1(): # $routeHandler - return "

hello

" # $HttpResponse $mimetype=text/html $responseBody="

hello

" + return "

hello

" # $HttpResponse mimetype=text/html responseBody="

hello

" @app.route("/html2") # $routeSetup="/html2" @@ -15,14 +15,14 @@ def html2(): # $routeHandler # note that response saved in a variable intentionally -- we wan the annotations to # show that we recognize the response creation, and not the return (hopefully). (and # do the same in the following of the file) - resp = make_response("

hello

") # $HttpResponse $mimetype=text/html $responseBody="

hello

" - return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp + resp = make_response("

hello

") # $HttpResponse mimetype=text/html responseBody="

hello

" + return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp @app.route("/html3") # $routeSetup="/html3" def html3(): # $routeHandler - resp = app.make_response("

hello

") # $HttpResponse $mimetype=text/html $responseBody="

hello

" - return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp + resp = app.make_response("

hello

") # $HttpResponse mimetype=text/html responseBody="

hello

" + return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp # TODO: Create test-cases for the many ways that `make_response` can be used @@ -31,38 +31,38 @@ def html3(): # $routeHandler @app.route("/html4") # $routeSetup="/html4" def html4(): # $routeHandler - resp = Response("

hello

") # $HttpResponse $mimetype=text/html $responseBody="

hello

" - return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp + resp = Response("

hello

") # $HttpResponse mimetype=text/html responseBody="

hello

" + return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp @app.route("/html5") # $routeSetup="/html5" def html5(): # $routeHandler # note: flask.Flask.response_class is set to `flask.Response` by default. # it can be overridden, but we don't try to handle that right now. - resp = Flask.response_class("

hello

") # $HttpResponse $mimetype=text/html $responseBody="

hello

" - return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp + resp = Flask.response_class("

hello

") # $HttpResponse mimetype=text/html responseBody="

hello

" + return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp @app.route("/html6") # $routeSetup="/html6" def html6(): # $routeHandler # note: app.response_class (flask.Flask.response_class) is set to `flask.Response` by default. # it can be overridden, but we don't try to handle that right now. - resp = app.response_class("

hello

") # $HttpResponse $mimetype=text/html $responseBody="

hello

" - return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp + resp = app.response_class("

hello

") # $HttpResponse mimetype=text/html responseBody="

hello

" + return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp @app.route("/html7") # $routeSetup="/html7" def html7(): # $routeHandler - resp = make_response() # $HttpResponse $mimetype=text/html - resp.set_data("

hello

") # $f-:responseBody="

hello

" - return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp + resp = make_response() # $HttpResponse mimetype=text/html + resp.set_data("

hello

") # $ MISSING: responseBody="

hello

" + return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp @app.route("/jsonify") # $routeSetup="/jsonify" def jsonify_route(): # $routeHandler data = {"foo": "bar"} - resp = jsonify(data) # $f-:HttpResponse $f-:mimetype=application/json $f-:responseBody=data - return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp + resp = jsonify(data) # $ MISSING: HttpResponse mimetype=application/json responseBody=data + return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp ################################################################################ # Tricky return handling @@ -73,19 +73,19 @@ def tricky_return1(): # $routeHandler if "raw" in request.args: resp = "

hellu

" else: - resp = make_response("

hello

") # $HttpResponse $mimetype=text/html $responseBody="

hello

" - return resp # $HttpResponse $mimetype=text/html $responseBody=resp + resp = make_response("

hello

") # $HttpResponse mimetype=text/html responseBody="

hello

" + return resp # $HttpResponse mimetype=text/html responseBody=resp def helper(): if "raw" in request.args: return "

hellu

" else: - return make_response("

hello

") # $HttpResponse $mimetype=text/html $responseBody="

hello

" + return make_response("

hello

") # $HttpResponse mimetype=text/html responseBody="

hello

" @app.route("/tricky-return2") # $routeSetup="/tricky-return2" def tricky_return2(): # $routeHandler resp = helper() - return resp # $HttpResponse $mimetype=text/html $responseBody=resp + return resp # $HttpResponse mimetype=text/html responseBody=resp ################################################################################ @@ -95,16 +95,16 @@ def tricky_return2(): # $routeHandler @app.route("/content-type/response-modification1") # $routeSetup="/content-type/response-modification1" def response_modification1(): # $routeHandler - resp = make_response("

hello

") # $HttpResponse $mimetype=text/html $responseBody="

hello

" - resp.content_type = "text/plain" # $f-:HttpResponse $f-:mimetype=text/plain - return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp + resp = make_response("

hello

") # $HttpResponse mimetype=text/html responseBody="

hello

" + resp.content_type = "text/plain" # $ MISSING: HttpResponse mimetype=text/plain + return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp @app.route("/content-type/response-modification2") # $routeSetup="/content-type/response-modification2" def response_modification2(): # $routeHandler - resp = make_response("

hello

") # $HttpResponse $mimetype=text/html $responseBody="

hello

" - resp.headers["content-type"] = "text/plain" # $f-:HttpResponse $f-:mimetype=text/plain - return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp + resp = make_response("

hello

") # $HttpResponse mimetype=text/html responseBody="

hello

" + resp.headers["content-type"] = "text/plain" # $ MISSING: HttpResponse mimetype=text/plain + return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp # Exploration of mimetype/content_type/headers arguments to `app.response_class` -- things can get tricky @@ -113,60 +113,60 @@ def response_modification2(): # $routeHandler @app.route("/content-type/Response1") # $routeSetup="/content-type/Response1" def Response1(): # $routeHandler - resp = Response("

hello

", mimetype="text/plain") # $HttpResponse $mimetype=text/plain $responseBody="

hello

" - return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp + resp = Response("

hello

", mimetype="text/plain") # $HttpResponse mimetype=text/plain responseBody="

hello

" + return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp @app.route("/content-type/Response2") # $routeSetup="/content-type/Response2" def Response2(): # $routeHandler - resp = Response("

hello

", content_type="text/plain; charset=utf-8") # $HttpResponse $mimetype=text/plain $responseBody="

hello

" - return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp + resp = Response("

hello

", content_type="text/plain; charset=utf-8") # $HttpResponse mimetype=text/plain responseBody="

hello

" + return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp @app.route("/content-type/Response3") # $routeSetup="/content-type/Response3" def Response3(): # $routeHandler # content_type argument takes priority (and result is text/plain) - resp = Response("

hello

", content_type="text/plain; charset=utf-8", mimetype="text/html") # $HttpResponse $mimetype=text/plain $responseBody="

hello

" - return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp + resp = Response("

hello

", content_type="text/plain; charset=utf-8", mimetype="text/html") # $HttpResponse mimetype=text/plain responseBody="

hello

" + return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp @app.route("/content-type/Response4") # $routeSetup="/content-type/Response4" def Response4(): # $routeHandler # note: capitalization of Content-Type does not matter - resp = Response("

hello

", headers={"Content-TYPE": "text/plain"}) # $HttpResponse $f+:mimetype=text/html $f-:mimetype=text/plain $responseBody="

hello

" - return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp + resp = Response("

hello

", headers={"Content-TYPE": "text/plain"}) # $HttpResponse responseBody="

hello

" SPURIOUS: mimetype=text/html MISSING: mimetype=text/plain + return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp @app.route("/content-type/Response5") # $routeSetup="/content-type/Response5" def Response5(): # $routeHandler # content_type argument takes priority (and result is text/plain) # note: capitalization of Content-Type does not matter - resp = Response("

hello

", headers={"Content-TYPE": "text/html"}, content_type="text/plain; charset=utf-8") # $HttpResponse $mimetype=text/plain $responseBody="

hello

" - return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp + resp = Response("

hello

", headers={"Content-TYPE": "text/html"}, content_type="text/plain; charset=utf-8") # $HttpResponse mimetype=text/plain responseBody="

hello

" + return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp @app.route("/content-type/Response6") # $routeSetup="/content-type/Response6" def Response6(): # $routeHandler # mimetype argument takes priority over header (and result is text/plain) # note: capitalization of Content-Type does not matter - resp = Response("

hello

", headers={"Content-TYPE": "text/html"}, mimetype="text/plain") # $HttpResponse $mimetype=text/plain $responseBody="

hello

" - return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp + resp = Response("

hello

", headers={"Content-TYPE": "text/html"}, mimetype="text/plain") # $HttpResponse mimetype=text/plain responseBody="

hello

" + return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp @app.route("/content-type/Flask-response-class") # $routeSetup="/content-type/Flask-response-class" def Flask_response_class(): # $routeHandler # note: flask.Flask.response_class is set to `flask.Response` by default. # it can be overridden, but we don't try to handle that right now. - resp = Flask.response_class("

hello

", mimetype="text/plain") # $HttpResponse $mimetype=text/plain $responseBody="

hello

" - return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp + resp = Flask.response_class("

hello

", mimetype="text/plain") # $HttpResponse mimetype=text/plain responseBody="

hello

" + return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp @app.route("/content-type/app-response-class") # $routeSetup="/content-type/app-response-class" def app_response_class(): # $routeHandler # note: app.response_class (flask.Flask.response_class) is set to `flask.Response` by default. # it can be overridden, but we don't try to handle that right now. - resp = app.response_class("

hello

", mimetype="text/plain") # $HttpResponse $mimetype=text/plain $responseBody="

hello

" - return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp + resp = app.response_class("

hello

", mimetype="text/plain") # $HttpResponse mimetype=text/plain responseBody="

hello

" + return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp # TODO: add tests for setting status code diff --git a/python/ql/test/experimental/library-tests/frameworks/stdlib/FileSystemAccess.py b/python/ql/test/experimental/library-tests/frameworks/stdlib/FileSystemAccess.py index 3d9fac82ed19..975f2c24bcfb 100644 --- a/python/ql/test/experimental/library-tests/frameworks/stdlib/FileSystemAccess.py +++ b/python/ql/test/experimental/library-tests/frameworks/stdlib/FileSystemAccess.py @@ -3,5 +3,5 @@ o = open -o("filepath") # f-:$getAPathArgument="filepath" -o(file="filepath") # f-:$getAPathArgument="filepath" +o("filepath") # $ MISSING: getAPathArgument="filepath" +o(file="filepath") # $ MISSING: getAPathArgument="filepath" diff --git a/python/ql/test/experimental/library-tests/frameworks/stdlib/SafeAccessCheck.py b/python/ql/test/experimental/library-tests/frameworks/stdlib/SafeAccessCheck.py index 355c1e3481c2..d5a727311ff0 100644 --- a/python/ql/test/experimental/library-tests/frameworks/stdlib/SafeAccessCheck.py +++ b/python/ql/test/experimental/library-tests/frameworks/stdlib/SafeAccessCheck.py @@ -1,8 +1,8 @@ s = "taintedString" -if s.startswith("tainted"): # $checks=s $branch=true +if s.startswith("tainted"): # $checks=s branch=true pass -sw = s.startswith # $f-:checks=s $f-:branch=true +sw = s.startswith # $ MISSING: checks=s branch=true if sw("safe"): pass From 870ed0039b27db6447a64752617815f61a9b4143 Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Sat, 31 Oct 2020 18:01:55 +0100 Subject: [PATCH 10/12] Python: Allow single quote strings and accept test changes. --- python/ql/test/TestUtilities/InlineExpectationsTest.qll | 2 +- .../library-tests/frameworks/django-v1/ConceptsTest.expected | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/python/ql/test/TestUtilities/InlineExpectationsTest.qll b/python/ql/test/TestUtilities/InlineExpectationsTest.qll index d5eebe733bbc..357984150787 100644 --- a/python/ql/test/TestUtilities/InlineExpectationsTest.qll +++ b/python/ql/test/TestUtilities/InlineExpectationsTest.qll @@ -233,7 +233,7 @@ private string expectationPattern() { exists(string tag, string tags, string value | tag = "[A-Za-z-_][A-Za-z-_0-9]*" and tags = "((?:" + tag + ")(?:\\s*,\\s*" + tag + ")*)" and - value = "((?:\"[^\"]*\"|\\S+)*)" and + value = "((?:\"[^\"]*\"|'[^']*'|\\S+)*)" and result = tags + "(?:=" + value + ")?" ) } diff --git a/python/ql/test/experimental/library-tests/frameworks/django-v1/ConceptsTest.expected b/python/ql/test/experimental/library-tests/frameworks/django-v1/ConceptsTest.expected index 78280a935cb5..6d7f3efe8118 100644 --- a/python/ql/test/experimental/library-tests/frameworks/django-v1/ConceptsTest.expected +++ b/python/ql/test/experimental/library-tests/frameworks/django-v1/ConceptsTest.expected @@ -1,5 +1,3 @@ -| response_test.py:16:12:16:86 | ControlFlowNode for HttpResponse() | Unexpected result: responseBody='' | -| response_test.py:16:89:16:171 | Comment # $HttpResponse mimetype=text/plain responseBody='' | Missing result:responseBody=' Date: Sat, 31 Oct 2020 18:02:12 +0100 Subject: [PATCH 11/12] C++: Sync identical file. --- cpp/ql/test/TestUtilities/InlineExpectationsTest.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/ql/test/TestUtilities/InlineExpectationsTest.qll b/cpp/ql/test/TestUtilities/InlineExpectationsTest.qll index d5eebe733bbc..357984150787 100644 --- a/cpp/ql/test/TestUtilities/InlineExpectationsTest.qll +++ b/cpp/ql/test/TestUtilities/InlineExpectationsTest.qll @@ -233,7 +233,7 @@ private string expectationPattern() { exists(string tag, string tags, string value | tag = "[A-Za-z-_][A-Za-z-_0-9]*" and tags = "((?:" + tag + ")(?:\\s*,\\s*" + tag + ")*)" and - value = "((?:\"[^\"]*\"|\\S+)*)" and + value = "((?:\"[^\"]*\"|'[^']*'|\\S+)*)" and result = tags + "(?:=" + value + ")?" ) } From 6d0783a3bd57b8f438c7e7ebdc70af6b28373c70 Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Sat, 31 Oct 2020 18:13:12 +0100 Subject: [PATCH 12/12] Python: Make sure that expected values with tag mimetype is wrapped in quotes if the value contains a space. --- .../frameworks/django-v1/ConceptsTest.expected | 8 -------- .../library-tests/frameworks/django-v1/response_test.py | 8 ++++---- python/ql/test/experimental/meta/ConceptsTest.qll | 9 ++++++++- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/python/ql/test/experimental/library-tests/frameworks/django-v1/ConceptsTest.expected b/python/ql/test/experimental/library-tests/frameworks/django-v1/ConceptsTest.expected index 6d7f3efe8118..e69de29bb2d1 100644 --- a/python/ql/test/experimental/library-tests/frameworks/django-v1/ConceptsTest.expected +++ b/python/ql/test/experimental/library-tests/frameworks/django-v1/ConceptsTest.expected @@ -1,8 +0,0 @@ -| response_test.py:21:12:21:56 | ControlFlowNode for HttpResponseRedirect() | Unexpected result: mimetype=text/html; charset=utf-8 | -| response_test.py:21:59:21:132 | Comment # $HttpResponse mimetype=text/html; charset=utf-8 responseBody=Attribute() | Missing result:mimetype=text/html; | -| response_test.py:25:12:25:56 | ControlFlowNode for HttpResponseNotFound() | Unexpected result: mimetype=text/html; charset=utf-8 | -| response_test.py:25:59:25:132 | Comment # $HttpResponse mimetype=text/html; charset=utf-8 responseBody=Attribute() | Missing result:mimetype=text/html; | -| response_test.py:32:16:32:29 | ControlFlowNode for HttpResponse() | Unexpected result: mimetype=text/html; charset=utf-8 | -| response_test.py:32:32:32:80 | Comment # $HttpResponse mimetype=text/html; charset=utf-8 | Missing result:mimetype=text/html; | -| response_test.py:33:5:33:43 | ControlFlowNode for Attribute() | Unexpected result: mimetype=text/html; charset=utf-8 | -| response_test.py:33:46:33:119 | Comment # $HttpResponse mimetype=text/html; charset=utf-8 responseBody=Attribute() | Missing result:mimetype=text/html; | diff --git a/python/ql/test/experimental/library-tests/frameworks/django-v1/response_test.py b/python/ql/test/experimental/library-tests/frameworks/django-v1/response_test.py index 2ef1e1b64e8f..01425ce38059 100644 --- a/python/ql/test/experimental/library-tests/frameworks/django-v1/response_test.py +++ b/python/ql/test/experimental/library-tests/frameworks/django-v1/response_test.py @@ -18,19 +18,19 @@ def safe__manual_content_type(request): # XSS FP reported in https://github.com/github/codeql/issues/3466 # Note: This should be an open-redirect sink, but not an XSS sink. def or__redirect(request): - return HttpResponseRedirect(request.GET.get("next")) # $HttpResponse mimetype=text/html; charset=utf-8 responseBody=Attribute() + return HttpResponseRedirect(request.GET.get("next")) # $HttpResponse mimetype="text/html; charset=utf-8" responseBody=Attribute() # Ensure that simple subclasses are still vuln to XSS def xss__not_found(request): - return HttpResponseNotFound(request.GET.get("name")) # $HttpResponse mimetype=text/html; charset=utf-8 responseBody=Attribute() + return HttpResponseNotFound(request.GET.get("name")) # $HttpResponse mimetype="text/html; charset=utf-8" responseBody=Attribute() # Ensure we still have an XSS sink when manually setting the content_type to HTML def xss__manual_response_type(request): return HttpResponse(request.GET.get("name"), content_type="text/html; charset=utf-8") # $HttpResponse mimetype=text/html responseBody=Attribute() def xss__write(request): - response = HttpResponse() # $HttpResponse mimetype=text/html; charset=utf-8 - response.write(request.GET.get("name")) # $HttpResponse mimetype=text/html; charset=utf-8 responseBody=Attribute() + response = HttpResponse() # $HttpResponse mimetype="text/html; charset=utf-8" + response.write(request.GET.get("name")) # $HttpResponse mimetype="text/html; charset=utf-8" responseBody=Attribute() # This is safe but probably a bug if the argument to `write` is not a result of `json.dumps` or similar. def safe__write_json(request): diff --git a/python/ql/test/experimental/meta/ConceptsTest.qll b/python/ql/test/experimental/meta/ConceptsTest.qll index 4a887b3733ac..479036bd8b13 100644 --- a/python/ql/test/experimental/meta/ConceptsTest.qll +++ b/python/ql/test/experimental/meta/ConceptsTest.qll @@ -178,7 +178,14 @@ class HttpServerHttpResponseTest extends InlineExpectationsTest { exists(HTTP::Server::HttpResponse response | location = response.getLocation() and element = response.toString() and - value = response.getMimetype() and + // Ensure that an expectation value such as "mimetype=text/html; charset=utf-8" is parsed as a + // single expectation with tag mimetype, and not as two expecations with tags mimetype and + // charset. + ( + if exists(response.getMimetype().indexOf(" ")) + then value = "\"" + response.getMimetype() + "\"" + else value = response.getMimetype() + ) and tag = "mimetype" ) )