+ * All paths and methods are handled by this class and it responds with a simple + * message indicating the method, path, and body (if applicable). + */ +@Path("/anything") +@WasmPlugin("waf") // use the corsaWAF filter +public class Anything { + + @GET + public Response gext(@Context HttpHeaders headers) { + return process(headers, null); + } + + @DELETE + public Response delete(HttpHeaders headers) { + return process(headers, null); + } + + @OPTIONS + public Response options(HttpHeaders headers) { + return process(headers, null); + } + + @HEAD + public Response head(HttpHeaders headers) { + return process(headers, null); + } + + @POST + public Response postx(HttpHeaders headers, String body) { + return process(headers, body); + } + + @PUT + public Response put(HttpHeaders headers, String body) { + return process(headers, body); + } + + @PATCH + public Response patch(HttpHeaders headers, String body) { + return process(headers, body); + } + + private Response process(HttpHeaders headers, String body) { + Response.ResponseBuilder builder = Response.ok(); + for (var header : headers.getRequestHeaders().entrySet()) { + for (String value : header.getValue()) { + builder = builder.header(header.getKey(), value); + } + } + if (body != null) { + builder = builder.entity(body); + } + return builder.build(); + } +} diff --git a/quarkus-x-corazawaf-example/src/main/java/io/roastedroot/proxywasm/corazawaf/example/App.java b/quarkus-x-corazawaf-example/src/main/java/io/roastedroot/proxywasm/corazawaf/example/App.java new file mode 100644 index 0000000..f481fb3 --- /dev/null +++ b/quarkus-x-corazawaf-example/src/main/java/io/roastedroot/proxywasm/corazawaf/example/App.java @@ -0,0 +1,45 @@ +package io.roastedroot.proxywasm.corazawaf.example; + +import com.dylibso.chicory.wasm.Parser; +import com.dylibso.chicory.wasm.WasmModule; +import io.roastedroot.proxywasm.LogHandler; +import io.roastedroot.proxywasm.StartException; +import io.roastedroot.proxywasm.plugin.Plugin; +import io.roastedroot.proxywasm.plugin.PluginFactory; +import io.roastedroot.proxywasm.plugin.SimpleMetricsHandler; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; + +@ApplicationScoped +public class App { + + private static WasmModule module = + Parser.parse(App.class.getResourceAsStream("coraza-proxy-wasm.wasm")); + + static final String CONFIG; + + static { + try (InputStream is = App.class.getResourceAsStream("waf-config.json")) { + CONFIG = new String(is.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + static final boolean DEBUG = "true".equals(System.getenv("DEBUG")); + + @Produces + public PluginFactory waf() throws StartException { + return () -> + Plugin.builder() + .withName("waf") + .withLogger(DEBUG ? LogHandler.SYSTEM : null) + .withPluginConfig(CONFIG) + .withMetricsHandler(new SimpleMetricsHandler()) + .build(module); + } +} diff --git a/quarkus-x-corazawaf-example/src/main/java/io/roastedroot/proxywasm/corazawaf/example/Status.java b/quarkus-x-corazawaf-example/src/main/java/io/roastedroot/proxywasm/corazawaf/example/Status.java new file mode 100644 index 0000000..ebbef1c --- /dev/null +++ b/quarkus-x-corazawaf-example/src/main/java/io/roastedroot/proxywasm/corazawaf/example/Status.java @@ -0,0 +1,53 @@ +package io.roastedroot.proxywasm.corazawaf.example; + +import io.roastedroot.proxywasm.jaxrs.WasmPlugin; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HEAD; +import jakarta.ws.rs.OPTIONS; +import jakarta.ws.rs.PATCH; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Response; + +@Path("/status/{status}") +@WasmPlugin("waf") // use the corsaWAF filter +public class Status { + + @GET + public Response gext(@PathParam("status") int status) { + return Response.status(status).build(); + } + + @DELETE + public Response delete(@PathParam("status") int status) { + return Response.status(status).build(); + } + + @OPTIONS + public Response options(@PathParam("status") int status) { + return Response.status(status).build(); + } + + @HEAD + public Response head(@PathParam("status") int status) { + return Response.status(status).build(); + } + + @POST + public Response postx(@PathParam("status") int status, String body) { + return Response.status(status).build(); + } + + @PUT + public Response put(@PathParam("status") int status, String body) { + return Response.status(status).build(); + } + + @PATCH + public Response patch(@PathParam("status") int status, String body) { + return Response.status(status).build(); + } +} diff --git a/quarkus-x-corazawaf-example/src/main/resources/application.properties b/quarkus-x-corazawaf-example/src/main/resources/application.properties new file mode 100644 index 0000000..6e516f2 --- /dev/null +++ b/quarkus-x-corazawaf-example/src/main/resources/application.properties @@ -0,0 +1,2 @@ +quarkus.log.level=INFO +quarkus.log.category."org.hibernate".level=DEBUG diff --git a/quarkus-x-corazawaf-example/src/main/resources/io/roastedroot/proxywasm/corazawaf/example/README.md b/quarkus-x-corazawaf-example/src/main/resources/io/roastedroot/proxywasm/corazawaf/example/README.md new file mode 100644 index 0000000..edf5116 --- /dev/null +++ b/quarkus-x-corazawaf-example/src/main/resources/io/roastedroot/proxywasm/corazawaf/example/README.md @@ -0,0 +1,6 @@ +## Attribution + +The coraza-proxy-wasm.wasm plugin comes from: + +https://github.com/corazawaf/coraza-proxy-wasm/releases/tag/0.5.0 + diff --git a/quarkus-x-corazawaf-example/src/main/resources/io/roastedroot/proxywasm/corazawaf/example/coraza-proxy-wasm.wasm b/quarkus-x-corazawaf-example/src/main/resources/io/roastedroot/proxywasm/corazawaf/example/coraza-proxy-wasm.wasm new file mode 100644 index 0000000..564ef58 Binary files /dev/null and b/quarkus-x-corazawaf-example/src/main/resources/io/roastedroot/proxywasm/corazawaf/example/coraza-proxy-wasm.wasm differ diff --git a/quarkus-x-corazawaf-example/src/main/resources/io/roastedroot/proxywasm/corazawaf/example/waf-config.json b/quarkus-x-corazawaf-example/src/main/resources/io/roastedroot/proxywasm/corazawaf/example/waf-config.json new file mode 100644 index 0000000..4f3608d --- /dev/null +++ b/quarkus-x-corazawaf-example/src/main/resources/io/roastedroot/proxywasm/corazawaf/example/waf-config.json @@ -0,0 +1,33 @@ +{ + "directives_map": { + "rs1": [ + "Include @demo-conf", + "Include @crs-setup-conf", + "SecDefaultAction \"phase:3,log,auditlog,pass\"", + "SecDefaultAction \"phase:4,log,auditlog,pass\"", + "SecDefaultAction \"phase:5,log,auditlog,pass\"", + "SecDebugLogLevel 9", + "Include @owasp_crs/*.conf", + "SecRule REQUEST_URI \"@streq /admin\" \"id:101,phase:1,t:lowercase,deny\" \nSecRule REQUEST_BODY \"@rx maliciouspayload\" \"id:102,phase:2,t:lowercase,deny\" \nSecRule RESPONSE_HEADERS::status \"@rx 406\" \"id:103,phase:3,t:lowercase,deny\" \nSecRule RESPONSE_BODY \"@contains responsebodycode\" \"id:104,phase:4,t:lowercase,deny\"" + ], + "rs2": [ + "Include @demo-conf", + "Include @crs-setup-conf", + "SecDefaultAction \"phase:3,log,auditlog,pass\"", + "SecDefaultAction \"phase:4,log,auditlog,pass\"", + "SecDefaultAction \"phase:5,log,auditlog,pass\"", + "SecDebugLogLevel 9", + "Include @owasp_crs/*.conf", + "SecRule REQUEST_URI \"@streq /example\" \"id:101,phase:1,t:lowercase,deny\" \nSecRule REQUEST_BODY \"@rx maliciouspayload\" \"id:102,phase:2,t:lowercase,deny\" \nSecRule RESPONSE_HEADERS::status \"@rx 406\" \"id:103,phase:3,t:lowercase,deny\" \nSecRule RESPONSE_BODY \"@contains responsebodycode\" \"id:104,phase:4,t:lowercase,deny\"" + ] + }, + "default_directives": "rs1", + "metric_labels": { + "owner": "coraza", + "identifier": "global" + }, + "per_authority_directives":{ + "foo.example.com":"rs2", + "bar.example.com":"rs2" + } +} \ No newline at end of file diff --git a/quarkus-x-corazawaf-example/src/test/java/io/roastedroot/proxywasm/corazawaf/example/ResourcesTest.java b/quarkus-x-corazawaf-example/src/test/java/io/roastedroot/proxywasm/corazawaf/example/ResourcesTest.java new file mode 100644 index 0000000..ca1c138 --- /dev/null +++ b/quarkus-x-corazawaf-example/src/test/java/io/roastedroot/proxywasm/corazawaf/example/ResourcesTest.java @@ -0,0 +1,148 @@ +package io.roastedroot.proxywasm.corazawaf.example; + +import static io.restassured.RestAssured.given; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +// Derived from: +// https://github.com/corazawaf/coraza-proxy-wasm?tab=readme-ov-file#manual-requests +@QuarkusTest +public class ResourcesTest { + + // # True positive requests: + + @Test + public void testPhase1() throws InterruptedException { + + // # Custom rule phase 1 + // curl -I 'http://localhost:8080/admin' + given().when().get("/admin").then().statusCode(403); + } + + @Test + // @Disabled("not yet working.") + public void testPhase2() throws InterruptedException { + + // # Custom rule phase 2 + // curl -i -X POST 'http://localhost:8080/anything' --data "maliciouspayload" + given().header("Content-Type", "application/x-www-form-urlencoded") + .body("maliciouspayload") + .when() + .post("/anything") + .then() + .statusCode(403); + } + + @Test + public void testPhase3() throws InterruptedException { + // # Custom rule phase 3 + // curl -I 'http://localhost:8080/status/406' + given().when().get("/status/406").then().statusCode(403); + } + + @Test + @Disabled( + "Seems like coraza is not loading the reponse body. it logs: 'Skipping response body" + + " processing tx_id=\"xxxx\" response_body_access=true'.") + public void testPhase4() throws InterruptedException { + + // # Custom rule phase 4 + // curl -i -X POST 'http://localhost:8080/anything' --data "responsebodycode" + given().header("Content-Type", "application/x-www-form-urlencoded") + .body("responsebodycode") + .when() + .post("/anything") + .then() + .statusCode(403); + } + + @Test + public void testXssPhase1() throws InterruptedException { + // # XSS phase 1 + // curl -I 'http://localhost:8080/anything?arg=' + given().when().get("/anything?arg=").then().statusCode(403); + } + + @Test + public void testSQLIPhase2() throws InterruptedException { + + // # SQLI phase 2 (reading the body request) + // curl -i -X POST 'http://localhost:8080/anything' --data "1%27%20ORDER%20BY%203--%2B" + given().header("Content-Type", "application/x-www-form-urlencoded") + .body("1%27%20ORDER%20BY%203--%2B") + .when() + .post("/anything") + .then() + .statusCode(403); + } + + @Test + public void testCRSScannerDetectionRule() throws InterruptedException { + // # Triggers a CRS scanner detection rule (913100) + // curl -I --user-agent "zgrab/0.1 (X11; U; Linux i686; en-US; rv:1.7)" + // -H "Host: localhost" + // -H "Accept: + // text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5" + // localhost:8080 + given().when() + .header("User-Agent", "zgrab/0.1 (X11; U; Linux i686; en-US; rv:1.7)") + .header("Host", "localhost") + .header( + "Accept", + "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5") + .get("/anything") + .then() + .statusCode(403); + } + + @Test + public void testNegativeRequest1() throws InterruptedException { + // # True negative requests: + // # A GET request with a harmless argument + // curl -I 'http://localhost:8080/anything?arg=arg_1' + given().when().get("/anything?arg=arg_1").then().statusCode(200); + } + + @Test + public void testNegativeRequest2() throws InterruptedException { + // # A payload (reading the body request) + // curl -i -X POST 'http://localhost:8080/anything' --data "This is a payload" + given().header("Content-Type", "application/x-www-form-urlencoded") + .body("This is a payload") + .when() + .post("/anything") + .then() + .statusCode(200); + } + + @Test + public void testNegativeRequest3() throws InterruptedException { + + // # An harmless response body + // curl -i -X POST 'http://localhost:8080/anything' --data "Hello world" + given().header("Content-Type", "application/x-www-form-urlencoded") + .body("Hello world") + .when() + .post("/anything") + .then() + .statusCode(200); + } + + @Test + public void testNegativeRequest4() throws InterruptedException { + + // # An usual user-agent + // curl -I --user-agent "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like + // Gecko) Chrome/105.0.0.0 Safari/537.36" localhost:8080 + given().when() + .header( + "User-Agent", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)" + + " Chrome/105.0.0.0 Safari/537.36") + .get("/anything") + .then() + .statusCode(200); + } +}