diff --git a/core/opentaint-java-querylang/samples/src/main/java/issues/iChain/HttpClient.java b/core/opentaint-java-querylang/samples/src/main/java/issues/iChain/HttpClient.java new file mode 100644 index 000000000..7aad55153 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/issues/iChain/HttpClient.java @@ -0,0 +1,15 @@ +package issues.iChain; + +/** + * Stub mirroring `java.net.http.HttpClient` — the sink consumes a + * built {@link HttpRequest}. + */ +public class HttpClient { + public static HttpClient newHttpClient() { + return new HttpClient(); + } + + public String send(HttpRequest req) { + return "response"; + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/issues/iChain/HttpRequest.java b/core/opentaint-java-querylang/samples/src/main/java/issues/iChain/HttpRequest.java new file mode 100644 index 000000000..6ef5aa71c --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/issues/iChain/HttpRequest.java @@ -0,0 +1,26 @@ +package issues.iChain; + +/** + * Stub mirroring `java.net.http.HttpRequest` for the chained-builder + * pattern repro. The static `newBuilder()` returns a {@link Builder}; + * the builder chain is `.uri(URI).GET().build()`. + */ +public class HttpRequest { + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + public Builder uri(URI uri) { + return this; + } + + public Builder GET() { + return this; + } + + public HttpRequest build() { + return new HttpRequest(); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/issues/iChain/Source.java b/core/opentaint-java-querylang/samples/src/main/java/issues/iChain/Source.java new file mode 100644 index 000000000..c7aa46559 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/issues/iChain/Source.java @@ -0,0 +1,12 @@ +package issues.iChain; + +/** + * Trivial tainted-source helper. The pattern-source in the rule is + * `String $UNTRUSTED = Source.taint();`, so any call to `taint()` + * produces an untrusted string. + */ +public class Source { + public static String taint() { + return "tainted"; + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/issues/iChain/URI.java b/core/opentaint-java-querylang/samples/src/main/java/issues/iChain/URI.java new file mode 100644 index 000000000..dd1d859d1 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/issues/iChain/URI.java @@ -0,0 +1,18 @@ +package issues.iChain; + +/** + * Stub mirroring `java.net.URI` for the chained-builder pattern repro. + * Real `java.net.URI` would also work but using a stub keeps the test + * self-contained. + */ +public class URI { + public final String value; + + private URI(String value) { + this.value = value; + } + + public static URI create(String s) { + return new URI(s); + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/issues/issueChain.java b/core/opentaint-java-querylang/samples/src/main/java/issues/issueChain.java new file mode 100644 index 000000000..f0657e62e --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/issues/issueChain.java @@ -0,0 +1,40 @@ +package issues; + +import base.RuleSample; +import base.RuleSet; +import issues.iChain.HttpClient; +import issues.iChain.HttpRequest; +import issues.iChain.Source; +import issues.iChain.URI; + +@RuleSet("issues/issueChain.yaml") +public abstract class issueChain implements RuleSample { + + // Carries real taint, but the rule's sub-pattern order (URL bound before the + // builder) doesn't line up with the code's call order (builder created first), + // so the sink is unreachable and the rule does not fire — a Negative case. + static class NegativeTaintOrderSensitive extends issueChain { + @Override + public void entrypoint() { + String t = Source.taint(); + HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create(t)) + .GET() + .build(); + HttpClient client = HttpClient.newHttpClient(); + client.send(req); + } + } + + static class NegativeTaint extends issueChain { + @Override + public void entrypoint() { + HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create("https://example.com")) + .GET() + .build(); + HttpClient client = HttpClient.newHttpClient(); + client.send(req); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/issues/issueChainSplitBuilder.java b/core/opentaint-java-querylang/samples/src/main/java/issues/issueChainSplitBuilder.java new file mode 100644 index 000000000..28dac39f2 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/issues/issueChainSplitBuilder.java @@ -0,0 +1,37 @@ +package issues; + +import base.RuleSample; +import base.RuleSet; +import issues.iChain.HttpClient; +import issues.iChain.HttpRequest; +import issues.iChain.Source; +import issues.iChain.URI; + +@RuleSet("issues/issueChainSplitBuilder.yaml") +public abstract class issueChainSplitBuilder implements RuleSample { + + static class PositiveTaint extends issueChainSplitBuilder { + @Override + public void entrypoint() { + String t = Source.taint(); + HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create(t)) + .GET() + .build(); + HttpClient client = HttpClient.newHttpClient(); + client.send(req); + } + } + + static class NegativeTaint extends issueChainSplitBuilder { + @Override + public void entrypoint() { + HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create("https://example.com")) + .GET() + .build(); + HttpClient client = HttpClient.newHttpClient(); + client.send(req); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/resources/issues/issueChain.yaml b/core/opentaint-java-querylang/samples/src/main/resources/issues/issueChain.yaml new file mode 100644 index 000000000..15fa292b6 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/issues/issueChain.yaml @@ -0,0 +1,22 @@ +rules: + - id: issue-chain + message: "Reproducer (order-sensitive match): rule sub-pattern order (URL before builder) doesn't line up with the code's call order, so the sink is unreachable" + severity: WARNING + languages: [java] + mode: taint + + pattern-sources: + - pattern: $UNTRUSTED = issues.iChain.Source.taint(); + + pattern-sinks: + - patterns: + - pattern-either: + - pattern-inside: | + $URL = issues.iChain.URI.create($UNTRUSTED); + ... + - pattern: | + $BUILDER = issues.iChain.HttpRequest.newBuilder().uri($URL); + ...; + $TYPE $REQ = $BUILDER.build(); + ...; + (issues.iChain.HttpClient $C).send($REQ, ...); diff --git a/core/opentaint-java-querylang/samples/src/main/resources/issues/issueChainSplitBuilder.yaml b/core/opentaint-java-querylang/samples/src/main/resources/issues/issueChainSplitBuilder.yaml new file mode 100644 index 000000000..8dd86d1b3 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/issues/issueChainSplitBuilder.yaml @@ -0,0 +1,25 @@ +rules: + - id: issue-chain-split-builder + message: "Working form: bind newBuilder() in its own pattern-inside, then match .uri()/.build() call-by-call" + severity: WARNING + languages: [java] + mode: taint + + pattern-sources: + - pattern: $UNTRUSTED = issues.iChain.Source.taint(); + + pattern-sinks: + - patterns: + - pattern-either: + - pattern-inside: | + $URL = issues.iChain.URI.create($UNTRUSTED); + ... + - pattern-inside: | + $NEW_BUILDER = issues.iChain.HttpRequest.newBuilder(); + ... + - pattern: | + $BUILDER = $NEW_BUILDER.uri($URL); + ...; + $TYPE $REQ = $BUILDER.build(); + ...; + (issues.iChain.HttpClient $C).send($REQ, ...); diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/IssuesTest.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/IssuesTest.kt index 185fe851c..9bcce3a57 100644 --- a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/IssuesTest.kt +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/IssuesTest.kt @@ -16,6 +16,8 @@ import issues.issue94 import issues.issue95 import issues.issue96 import issues.issue97 +import issues.issueChain +import issues.issueChainSplitBuilder import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.TestInstance @@ -105,6 +107,12 @@ class IssuesTest : SampleBasedTest() { @Test fun `issue 97`() = runTest() + @Test + fun `issue chain-pattern order-sensitive match`() = runTest(EXPECT_STATE_VAR) + + @Test + fun `issue chain-pattern split builder`() = runTest(EXPECT_STATE_VAR) + @AfterAll fun close() { closeRunner()