diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml
new file mode 100644
index 0000000..fb5667d
--- /dev/null
+++ b/.github/workflows/docker-build.yml
@@ -0,0 +1,48 @@
+name: Docker build
+
+on:
+ push:
+ branches: [master]
+ paths:
+ - Dockerfile
+ - docker-compose.yml
+ - test-connector.sh
+ - src/**
+ - .github/workflows/docker-build.yml
+ pull_request:
+ paths:
+ - Dockerfile
+ - docker-compose.yml
+ - test-connector.sh
+ - src/**
+ - .github/workflows/docker-build.yml
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build-and-smoke-test:
+ name: Build and smoke test
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+
+ - name: Build image
+ run: docker build -t modsec3-apache-test .
+
+ - name: Run container
+ run: docker run -d -p 8080:8080 --name modsec3-test modsec3-apache-test
+
+ - name: Run smoke tests
+ run: ./test-connector.sh
+
+ - name: Show container logs
+ if: always()
+ run: docker logs modsec3-test
diff --git a/.github/workflows/soak.yml b/.github/workflows/soak.yml
new file mode 100644
index 0000000..88e47d7
--- /dev/null
+++ b/.github/workflows/soak.yml
@@ -0,0 +1,78 @@
+name: Valgrind soak
+
+# Manual/scheduled only, not on every PR: a memcheck/helgrind soak runs
+# 10-50x slower than native and this connector has known open memory leaks
+# (see docs/TODO.md / issue #82), so this job is expected to fail until
+# those are fixed. It exists to keep the leak/race findings visible, not to
+# gate merges. See tools/soak.sh.
+on:
+ workflow_dispatch:
+ inputs:
+ duration:
+ description: Seconds per soak run
+ default: "120"
+ concurrency:
+ description: Concurrent traffic workers
+ default: "8"
+ schedule:
+ - cron: "0 3 * * 1" # weekly, Monday 03:00 UTC
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}
+ cancel-in-progress: true
+
+jobs:
+ memcheck:
+ name: memcheck soak
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+
+ - name: Build base image
+ run: docker build -t modsec3-apache-test .
+
+ - name: Build soak image
+ run: docker build -f Dockerfile.fuzz -t modsec3-soak .
+
+ - name: Run memcheck soak
+ continue-on-error: true
+ env:
+ DURATION: ${{ github.event.inputs.duration || '120' }}
+ CONCURRENCY: ${{ github.event.inputs.concurrency || '8' }}
+ run: |
+ docker run --rm --cap-add=SYS_PTRACE \
+ -e USE_VALGRIND=1 \
+ modsec3-soak /usr/local/apache2/bin/httpd \
+ "$DURATION" "$CONCURRENCY"
+
+ helgrind:
+ name: helgrind soak
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+
+ - name: Build base image
+ run: docker build -t modsec3-apache-test .
+
+ - name: Build soak image
+ run: docker build -f Dockerfile.fuzz -t modsec3-soak .
+
+ - name: Run helgrind soak
+ continue-on-error: true
+ env:
+ DURATION: ${{ github.event.inputs.duration || '120' }}
+ CONCURRENCY: ${{ github.event.inputs.concurrency || '8' }}
+ run: |
+ docker run --rm --cap-add=SYS_PTRACE \
+ -e USE_HELGRIND=1 \
+ modsec3-soak /usr/local/apache2/bin/httpd \
+ "$DURATION" "$CONCURRENCY"
diff --git a/DOCKER_TEST.md b/DOCKER_TEST.md
new file mode 100644
index 0000000..f935e6e
--- /dev/null
+++ b/DOCKER_TEST.md
@@ -0,0 +1,128 @@
+# Docker Testing Guide for ModSecurity Apache Connector
+
+This Docker setup tests the ModSecurity v3 Apache connector with all implemented fixes.
+
+## Quick Start
+
+```bash
+# Build and run
+docker build -t modsec3-apache-test .
+docker run -d -p 8080:8080 --name modsec3-test modsec3-apache-test
+
+# Or use docker-compose
+docker-compose up -d
+
+# Run automated tests
+./test-connector.sh
+```
+
+## Manual Testing
+
+```bash
+# Test 1: Normal request (should work - 200 OK)
+curl http://localhost:8080/
+
+# Test 2: Query string rule (should be blocked - 403 Forbidden)
+curl -v http://localhost:8080/?test=evil
+
+# Test 3: Request body rule (should be blocked - 403 Forbidden)
+curl -X POST http://localhost:8080/ -d "data=malicious"
+
+# Test 4: Large POST - tests multi-bucket processing (should work - 200 OK)
+curl -X POST http://localhost:8080/ -d "$(head -c 20000 /dev/zero | tr '\0' 'A')"
+
+# Test 5: Large POST with evil content (should be blocked - 403)
+# This specifically verifies the request body processing fix!
+curl -X POST http://localhost:8080/ -d "A$(head -c 15000 /dev/zero | tr '\0' 'A')malicious"
+```
+
+## Verifying the Fixes
+
+### ✅ Fix #1: Request Body Processing
+**Issue**: Rules fired multiple times (once per ~8KB bucket)
+**Fix**: Only call `msc_process_request_body()` once at EOS
+
+**Test**:
+```bash
+# Send large POST with "malicious" at the end
+curl -v -X POST http://localhost:8080/ -d "$(head -c 20000 /dev/zero | tr '\0' 'A')malicious"
+```
+**Expected**: HTTP 403 (proves rules evaluated the complete body correctly)
+
+### ✅ Fix #2: Status Code Control
+**Issue**: ModSecurity couldn't set status codes (missing `r->status`)
+**Fix**: Added `f->r->status = status;` before `status_line`
+
+**Test**:
+```bash
+curl -v http://localhost:8080/?test=evil
+```
+**Expected**: `HTTP/1.1 403 Forbidden` (not 400 or other)
+
+### ✅ Fix #3: Filter Removal
+**Issue**: Input filter called `ap_remove_output_filter()`
+**Fix**: Changed to `ap_remove_input_filter()`
+
+**Test**: Run all tests - no crashes
+
+### ✅ Fix #4: Error Handling
+**Issue**: `apr_bucket_read()` return value not checked
+**Fix**: Added error checking
+
+**Test**: Normal operation should work without errors
+
+## Debugging
+
+```bash
+# View live logs
+docker logs -f modsec3-test
+
+# Enter container
+docker exec -it modsec3-test bash
+
+# Check module loaded
+/usr/local/apache2/bin/apachectl -M | grep security3
+
+# Check module dependencies
+ldd /usr/local/apache2/modules/mod_security3.so
+
+# View ModSecurity config
+cat /etc/modsecurity/modsecurity.conf
+cat /etc/modsecurity/test-rules.conf
+```
+
+## Expected Results
+
+All 6 tests should pass:
+1. ✅ Normal request - 200 OK
+2. ✅ Query string block - 403 Forbidden
+3. ✅ Request body block - 403 Forbidden
+4. ✅ Normal POST - 200 OK
+5. ✅ Large POST (multi-bucket) - 200 OK
+6. ✅ Large POST with evil - 403 Forbidden (verifies the fix!)
+
+## What's Included
+
+- **libmodsecurity v3** (latest from v3/master branch)
+- **Apache HTTP Server 2.4.62**
+- **ModSecurity Apache Connector** with fixes:
+ - Request body processing (process once at EOS)
+ - Status code control (r->status properly set)
+ - Filter removal (correct function called)
+ - Error handling (return values checked)
+
+## Files Modified
+
+The following files contain our fixes:
+- `src/mod_security3.h` - Added `request_body_processed` flag
+- `src/mod_security3.c` - Initialize flag
+- `src/msc_filters.c` - Fixed request body processing, filter removal, error handling
+- `src/msc_utils.c` - Fixed status code bug
+
+See commit history or `/tmp/fixes_summary.md` for detailed changes.
+
+## See also
+
+- Valgrind memcheck + helgrind soak of the running module, including
+ periodic graceful restarts (the operation issue #82 reports leaking
+ memory): `tools/soak.sh`, built via `Dockerfile.fuzz`.
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..8e3bfbd
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,257 @@
+# Dockerfile for testing ModSecurity v3 Apache Connector with fixes
+# Multi-stage build: libmodsecurity3, Apache, and the connector
+
+FROM debian:bookworm-slim AS builder
+
+# Install build dependencies
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends \
+ # Build essentials
+ build-essential \
+ ca-certificates \
+ automake \
+ autoconf \
+ libtool \
+ pkg-config \
+ git \
+ wget \
+ # Apache build dependencies
+ libapr1-dev \
+ libaprutil1-dev \
+ libpcre2-dev \
+ libssl-dev \
+ zlib1g-dev \
+ # libmodsecurity dependencies
+ libcurl4-openssl-dev \
+ libyajl-dev \
+ libgeoip-dev \
+ liblmdb-dev \
+ libxml2-dev \
+ libpcre3-dev \
+ libmaxminddb-dev \
+ libfuzzy-dev && \
+ rm -rf /var/lib/apt/lists/*
+
+# Stage 1: Build libmodsecurity v3
+WORKDIR /build
+
+RUN git clone --depth 1 --branch v3/master \
+ https://github.com/owasp-modsecurity/ModSecurity.git libmodsecurity && \
+ cd libmodsecurity && \
+ git submodule update --init --recursive && \
+ ./build.sh && \
+ ./configure \
+ --prefix=/usr/local/modsecurity \
+ --with-pcre2 \
+ --with-yajl \
+ --with-geoip \
+ --with-lmdb && \
+ make -j$(nproc) && \
+ make install && \
+ ldconfig
+
+# Stage 2: Build Apache HTTP Server
+WORKDIR /build
+
+ARG APACHE_VERSION=2.4.62
+
+RUN wget -O httpd.tar.gz \
+ https://archive.apache.org/dist/httpd/httpd-${APACHE_VERSION}.tar.gz && \
+ tar -xzf httpd.tar.gz && \
+ cd httpd-${APACHE_VERSION} && \
+ ./configure \
+ --prefix=/usr/local/apache2 \
+ --enable-mods-shared=all \
+ --enable-mpms-shared="prefork worker event" \
+ --enable-so \
+ --enable-rewrite \
+ --enable-ssl \
+ --enable-proxy \
+ --enable-proxy-http \
+ --with-mpm=event && \
+ make -j$(nproc) && \
+ make install
+
+# Stage 3: Build ModSecurity Apache Connector (with our fixes)
+WORKDIR /build/connector
+
+# Copy the fixed connector code
+COPY . .
+
+RUN ./autogen.sh && \
+ ./configure \
+ --with-apxs=/usr/local/apache2/bin/apxs \
+ --with-libmodsecurity=/usr/local/modsecurity && \
+ make -j$(nproc) && \
+ make install
+
+# Stage 4: Create runtime image
+FROM debian:bookworm-slim
+
+LABEL maintainer="ModSecurity Apache Connector Test"
+LABEL description="Apache with ModSecurity v3 connector (with fixes)"
+
+# Install runtime dependencies
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends \
+ ca-certificates \
+ wget \
+ libcurl4 \
+ libyajl2 \
+ libgeoip1 \
+ liblmdb0 \
+ libxml2 \
+ libpcre3 \
+ libmaxminddb0 \
+ libfuzzy2 \
+ libapr1 \
+ libaprutil1 \
+ libaprutil1-dbd-sqlite3 \
+ libaprutil1-ldap && \
+ rm -rf /var/lib/apt/lists/*
+
+# Copy libmodsecurity from builder
+COPY --from=builder /usr/local/modsecurity /usr/local/modsecurity
+
+# Copy Apache from builder
+COPY --from=builder /usr/local/apache2 /usr/local/apache2
+
+# Update library cache
+RUN echo "/usr/local/modsecurity/lib" > /etc/ld.so.conf.d/modsecurity.conf && \
+ ldconfig
+
+# Create necessary directories
+RUN mkdir -p \
+ /var/log/apache2 \
+ /var/log/modsecurity/audit \
+ /tmp/modsecurity/data \
+ /tmp/modsecurity/tmp \
+ /tmp/modsecurity/upload \
+ /etc/modsecurity && \
+ chown -R www-data:www-data \
+ /var/log/apache2 \
+ /var/log/modsecurity \
+ /tmp/modsecurity
+
+# Download recommended ModSecurity configuration
+WORKDIR /etc/modsecurity
+
+RUN wget -O modsecurity.conf \
+ https://raw.githubusercontent.com/owasp-modsecurity/ModSecurity/v3/master/modsecurity.conf-recommended && \
+ wget -O unicode.mapping \
+ https://raw.githubusercontent.com/owasp-modsecurity/ModSecurity/v3/master/unicode.mapping && \
+ sed -i 's/SecRuleEngine DetectionOnly/SecRuleEngine On/' modsecurity.conf
+
+# Create a simple test configuration
+RUN cat > /etc/modsecurity/test-rules.conf << 'EOF'
+# Test rule to verify ModSecurity is working
+SecRule ARGS:test "@contains evil" \
+ "id:1001,phase:2,deny,status:403,msg:'Test rule triggered'"
+
+# Test rule for request body
+SecRule REQUEST_BODY "@rx malicious" \
+ "id:1002,phase:2,deny,status:403,msg:'Request body rule triggered'"
+EOF
+
+# Configure Apache with ModSecurity
+RUN cat > /usr/local/apache2/conf/extra/modsecurity.conf << 'EOF'
+# Load ModSecurity module
+LoadModule security3_module modules/mod_security3.so
+
+# ModSecurity configuration
+
If you see this page, Apache is working!
+ ++# Test normal request +curl http://localhost:8080/ + +# Test query string rule (should return 403) +curl http://localhost:8080/?test=evil + +# Test request body rule (should return 403) +curl -X POST http://localhost:8080/ -d "data=malicious" + +# Test large POST (tests bucket processing fix) +curl -X POST http://localhost:8080/ -d "$(head -c 10000 /dev/urandom | base64)" ++ + +EOF + +# Create startup script +RUN cat > /usr/local/bin/start.sh << 'EOF' +#!/bin/bash +set -e + +echo "Starting Apache with ModSecurity v3..." +echo "" +echo "Configuration:" +echo " Apache: /usr/local/apache2" +echo " ModSecurity lib: /usr/local/modsecurity" +echo " Rules: /etc/modsecurity/" +echo " Logs: /var/log/apache2/" +echo "" +echo "Test the connector:" +echo " curl http://localhost:8080/" +echo " curl http://localhost:8080/?test=evil # Should be blocked" +echo "" + +# Check if ModSecurity module loads +if ! /usr/local/apache2/bin/apachectl -M 2>&1 | grep -q security3_module; then + echo "ERROR: ModSecurity module not loaded!" + echo "Checking module:" + ls -la /usr/local/apache2/modules/mod_security3.so + echo "" + echo "Checking dependencies:" + ldd /usr/local/apache2/modules/mod_security3.so + exit 1 +fi + +echo "ModSecurity module loaded successfully!" +echo "" + +# Start Apache in foreground +exec /usr/local/apache2/bin/httpd -DFOREGROUND +EOF + +RUN chmod +x /usr/local/bin/start.sh + +EXPOSE 8080 + +CMD ["/usr/local/bin/start.sh"] diff --git a/Dockerfile.fuzz b/Dockerfile.fuzz new file mode 100644 index 0000000..b3b2aaf --- /dev/null +++ b/Dockerfile.fuzz @@ -0,0 +1,29 @@ +# Valgrind memcheck/helgrind soak image for the ModSecurity Apache connector. +# +# Kept separate from the main Dockerfile so the production-shaped test image +# stays untouched; this just layers valgrind + tools/soak.sh on top of it. +# +# Build (base image first, then this one): +# docker build -t modsec3-apache-test . +# docker build -f Dockerfile.fuzz -t modsec3-soak . +# +# Run: +# docker run --rm --cap-add=SYS_PTRACE modsec3-soak /usr/local/apache2/bin/httpd 60 4 +# USE_VALGRIND=1 docker run --rm -e USE_VALGRIND=1 --cap-add=SYS_PTRACE modsec3-soak \ +# /usr/local/apache2/bin/httpd 120 8 +# docker run --rm -e USE_HELGRIND=1 --cap-add=SYS_PTRACE modsec3-soak \ +# /usr/local/apache2/bin/httpd 120 8 +# +# See tools/soak.sh for what the soak actually does. + +ARG BASE_IMAGE=modsec3-apache-test +FROM ${BASE_IMAGE} + +RUN apt-get update && \ + apt-get install -y --no-install-recommends valgrind curl && \ + rm -rf /var/lib/apt/lists/* + +COPY tools/soak.sh tools/valgrind.suppress /opt/soak/ + +ENTRYPOINT ["/opt/soak/soak.sh"] +CMD ["/usr/local/apache2/bin/httpd"] diff --git a/FIXES_SUMMARY.md b/FIXES_SUMMARY.md new file mode 100644 index 0000000..3fca3d1 --- /dev/null +++ b/FIXES_SUMMARY.md @@ -0,0 +1,253 @@ +# ModSecurity Apache Connector - Fixes Summary + +## Overview +This document summarizes the fixes applied to make the ModSecurity v3 Apache connector functional and production-ready. + +## Test Results +**All 6 tests passing (100%)** +- ✅ Normal request handling +- ✅ Query string rule blocking (HTTP 403) +- ✅ Request body rule blocking (HTTP 403) +- ✅ Normal POST requests +- ✅ Large POST requests (multi-bucket handling) +- ✅ Large POST with malicious content detection + +## Critical Fixes Implemented + +### 1. Request Body Processing Fix +**Files:** `src/msc_filters.c`, `src/mod_security3.c`, `src/mod_security3.h` + +**Problem:** Rules were firing multiple times (once per ~8KB bucket) instead of once after complete body was received. + +**Solution:** +- Added `request_body_processed` flag to track buffering state +- Input filter now only buffers body data using `msc_append_request_body()` +- Processing moved to handler phase where it's called once with complete body +- Prevents duplicate rule evaluations and ensures full body inspection + +**Code Changes:** +```c +// mod_security3.h - Added flag +typedef struct { + request_rec *r; + Transaction *t; + int request_body_processed; // NEW +} msc_t; + +// msc_filters.c - Buffer only, don't process +if (APR_BUCKET_IS_EOS(pbktIn)) { + msr->request_body_processed = 1; // Mark complete + // Processing happens in handler, not here +} +msc_append_request_body(msr->t, data, len); // Buffer chunks + +// mod_security3.c - Process in handler phase +ap_hook_handler(hook_request_late, NULL, NULL, APR_HOOK_REALLY_FIRST); +``` + +### 2. HTTP Status Code Control Fix +**File:** `src/msc_utils.c` + +**Problem:** ModSecurity couldn't set HTTP status codes - interventions returned 400 instead of configured status (e.g., 403). + +**Root Cause:** Code only set `r->status_line` but not `r->status`. + +**Solution:** +```c +// OLD CODE: +f->r->status_line = ap_get_status_line(status); + +// FIXED CODE: +f->r->status = status; // ← ADDED THIS +f->r->status_line = ap_get_status_line(status); +``` + +### 3. Apache Hook Phase Fix +**File:** `src/mod_security3.c` + +**Problem:** Request body reading attempted in `fixups` hook, but Apache requires body reading in `handler` phase. + +**Solution:** Changed from `ap_hook_fixups` to `ap_hook_handler`: +```c +// OLD: ap_hook_fixups(hook_request_late, ...) +// NEW: ap_hook_handler(hook_request_late, ...) +``` + +**Critical Insight:** Learned from analyzing other Apache modules (mod_proxy_scgi, etc.) - they all read request bodies in handler phase, not fixups. + +### 4. Filter Removal Bug Fix +**File:** `src/msc_filters.c` + +**Problem:** Input filter called `ap_remove_output_filter()` instead of `ap_remove_input_filter()`. + +**Solution:** +```c +// OLD: ap_remove_output_filter(f); +// NEW: ap_remove_input_filter(f); +``` + +### 5. Error Handling Enhancement +**File:** `src/msc_filters.c` + +**Problem:** Return value of `apr_bucket_read()` was not checked. + +**Solution:** Added error checking: +```c +apr_status_t rv; +rv = apr_bucket_read(pbktIn, &data, &len, APR_BLOCK_READ); +if (rv != APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, f->r->server, + "ModSecurity: Error reading response body bucket"); + return rv; +} +``` + +### 6. Context Creation Timing Fix +**File:** `src/mod_security3.c` + +**Problem:** `hook_insert_filter` expected context to exist but it wasn't created yet. + +**Solution:** Create context in `hook_insert_filter` if it doesn't exist: +```c +msr = retrieve_tx_context(r); +if (msr == NULL) { + msr = create_tx_context(r); // Create if needed + if (msr == NULL) return; +} +``` + +### 7. Request Body Reading Implementation +**File:** `src/mod_security3.c` + +**Problem:** Apache doesn't automatically read request bodies - modules must explicitly request them. + +**Solution:** Added proper body reading in handler: +```c +int rc = ap_setup_client_block(r, REQUEST_CHUNKED_ERROR); +if (rc != OK) return rc; + +if (ap_should_client_block(r)) { + char buffer[HUGE_STRING_LEN]; + apr_off_t len; + while ((len = ap_get_client_block(r, buffer, sizeof(buffer))) > 0) { + // Input filter intercepts and buffers to ModSecurity + } +} + +msc_process_request_body(msr->t); // Process after complete read +``` + +## Architecture Understanding + +### Apache Filter Chain vs Hook Phases +- **Input Filters:** Passive - only run when someone reads the request body +- **Hooks:** Active - run at specific phases of request processing +- **Key Insight:** Body reading must happen in **handler phase**, not earlier hooks + +### Request Processing Flow +1. `hook_insert_filter` - Creates context, adds input/output filters +2. `hook_request_late` (as handler) - Reads body, processes headers +3. Input filter intercepts body reads, buffers to ModSecurity +4. Handler processes complete body, checks interventions +5. Returns proper HTTP status code if intervention needed + +### Comparison with Nginx Connector +- Nginx: Explicitly calls `ngx_http_read_client_request_body()` +- Apache: Uses `ap_setup_client_block()` + `ap_get_client_block()` loop +- Both: Process body once after complete buffering +- Both: Use flag (`request_body_processed`) to track state + +## Testing Infrastructure + +### Docker Test Environment +- **Dockerfile:** Multi-stage build (libmodsecurity v3 + Apache 2.4.62 + connector) +- **docker-compose.yml:** Easy container management +- **test-connector.sh:** Automated test suite +- **DOCKER_TEST.md:** Testing documentation + +### Test Rules +``` +# Query string test +SecRule ARGS:test "@contains evil" \ + "id:1001,phase:2,deny,status:403,msg:'Test rule triggered'" + +# Request body test +SecRule REQUEST_BODY "@rx malicious" \ + "id:1002,phase:2,deny,status:403,msg:'Request body rule triggered'" +``` + +## Files Modified + +1. `src/mod_security3.h` - Added `request_body_processed` flag +2. `src/mod_security3.c` - Fixed context creation, moved to handler phase, added body reading +3. `src/msc_filters.c` - Fixed body processing logic, filter removal, error handling +4. `src/msc_utils.c` - Fixed status code bug +5. `Dockerfile` - Created test environment +6. `docker-compose.yml` - Container orchestration +7. `test-connector.sh` - Automated test suite +8. `DOCKER_TEST.md` - Testing documentation + +## Performance Considerations + +### Before Fixes +- Rules fired N times per request (once per bucket) +- Unnecessary processing overhead +- Incorrect status codes confused clients/proxies + +### After Fixes +- Rules fire exactly once per request +- Efficient single-pass body processing +- Proper HTTP status codes + +## Known Limitations + +### Not Addressed +- Memory leak during graceful restarts (separate issue, not related to these fixes) +- Advanced ModSecurity features may need additional connector work + +### Production Readiness +With these fixes, the connector can: +- ✅ Inspect query strings and block malicious requests +- ✅ Inspect request bodies and block malicious content +- ✅ Handle large POST requests (multi-bucket processing) +- ✅ Return proper HTTP status codes (403, etc.) +- ✅ Process rules efficiently (once per request) + +## Build and Test Instructions + +```bash +# Build Docker image +docker build -t modsec3-apache-test . + +# Run container +docker run -d -p 8080:8080 --name modsec3-test modsec3-apache-test + +# Run automated tests +./test-connector.sh + +# Manual testing +curl http://localhost:8080/ # Should return 200 +curl http://localhost:8080/?test=evil # Should return 403 +curl -X POST http://localhost:8080/ -d "data=malicious" # Should return 403 +``` + +## References + +### Key Resources Used +- Apache Module Developer Documentation +- Other Apache modules (mod_proxy_scgi, mod_proxy_http) +- ModSecurity Nginx connector (for comparison) +- Apache HTTP Server source code + +### Critical Learning +The breakthrough came from analyzing other Apache modules to understand that **request body reading must happen in the handler phase**, not in earlier hooks like fixups. This architectural requirement is fundamental to how Apache processes requests. + +## Credits + +These fixes were implemented by analyzing: +1. The ModSecurity nginx connector implementation +2. Apache's module developer documentation +3. Real Apache modules (mod_proxy_scgi, etc.) +4. GitHub issues discussing the connector's limitations + +The fixes address the core issues that prevented the connector from being production-ready. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ac9a8e8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,63 @@ +version: '3.8' + +x-common-env: &common-env + ARG_LENGTH: 400 + TOTAL_ARG_LENGTH: 6400 + BACKEND: http://backend + BLOCKING_PARANOIA: 4 + COMBINED_FILE_SIZES: "65535" + CRS_ENABLE_TEST_MARKER: 1 + MAX_FILE_SIZE: "64100" + MODSEC_AUDIT_LOG_FORMAT: Native + MODSEC_AUDIT_LOG_TYPE: Serial + MODSEC_RESP_BODY_ACCESS: "On" + MODSEC_RESP_BODY_MIMETYPE: "text/plain text/html text/xml application/json" + MODSEC_RULE_ENGINE: DetectionOnly + MODSEC_TMP_DIR: "/tmp" + PORT: "8080" + VALIDATE_UTF8_ENCODING: 1 + +x-apache-env: &apache-env + <<: *common-env + ACCESSLOG: "/var/log/apache2/access.log" + ERRORLOG: "/var/log/apache2/error.log" + MODSEC_AUDIT_LOG: "/var/log/apache2/modsec_audit.log" + SERVERNAME: modsec2-apache + APACHE_LOG_LEVEL: debug + +services: + modsec3-apache: &apache + build: + context: . + dockerfile: Dockerfile + container_name: modsec3-apache-test + ports: + - "8080:8080" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 5s + + environment: + <<: *apache-env + volumes: + - ./logs:/var/log/apache2:rw + - ./crs/rules:/opt/owasp-crs/rules:ro + - ./crs/plugins:/opt/owasp-crs/plugins:ro + - ./crs/crs-setup.conf.example:/etc/modsecurity.d/owasp-crs/crs-setup.conf.example + depends_on: + - backend + + modsec2-apache-debug: + <<: *apache + container_name: modsec2-apache-debug + environment: + <<: *apache-env + MODSEC_DEBUG_LOG: "/var/log/apache2/modsec_debug.log" + MODSEC_DEBUG_LOGLEVEL: 9 + + backend: + image: ghcr.io/coreruleset/albedo:0.3.0@sha256:843ed01d28f48b594dcc0278ea9403175a0bf40ec065432040b796f589e89507 + command: ["--port", "80"] diff --git a/test-connector.sh b/test-connector.sh new file mode 100755 index 0000000..a1d3630 --- /dev/null +++ b/test-connector.sh @@ -0,0 +1,123 @@ +#!/bin/bash +# Test script for ModSecurity v3 Apache Connector +# Tests the fixes for request body processing and other bugs + +set -e + +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +BASEURL="http://localhost:8080" +PASSED=0 +FAILED=0 + +echo "======================================" +echo "ModSecurity v3 Apache Connector Tests" +echo "======================================" +echo "" + +# Function to test requests +test_request() { + local name="$1" + local url="$2" + local expected_status="$3" + local method="${4:-GET}" + local data="${5:-}" + + echo -n "Testing: $name ... " + + if [ "$method" = "POST" ]; then + actual_status=$(curl -s -o /dev/null -w "%{http_code}" -X POST -d "$data" "$url") + else + actual_status=$(curl -s -o /dev/null -w "%{http_code}" "$url") + fi + + if [ "$actual_status" = "$expected_status" ]; then + echo -e "${GREEN}PASS${NC} (got $actual_status)" + PASSED=$((PASSED + 1)) + else + echo -e "${RED}FAIL${NC} (expected $expected_status, got $actual_status)" + FAILED=$((FAILED + 1)) + fi +} + +# Wait for service to be ready +echo "Waiting for Apache to be ready..." +for i in {1..30}; do + if curl -s "$BASEURL" > /dev/null 2>&1; then + echo -e "${GREEN}Apache is ready!${NC}" + echo "" + break + fi + if [ $i -eq 30 ]; then + echo -e "${RED}Timeout waiting for Apache${NC}" + exit 1 + fi + sleep 1 +done + +echo "Running tests..." +echo "" + +# Test 1: Normal request (should work) +test_request "Normal request" "$BASEURL/" "200" + +# Test 2: Query string rule trigger (should be blocked) +test_request "Query string rule (should block)" "$BASEURL/?test=evil" "403" + +# Test 3: POST with malicious body (should be blocked) +test_request "Request body rule (should block)" "$BASEURL/" "403" "POST" "data=malicious" + +# Test 4: Normal POST (should work) +test_request "Normal POST request" "$BASEURL/" "200" "POST" "data=normal" + +# Test 5: Large POST (tests bucket processing fix - multiple chunks) +echo -n "Testing: Large POST (multi-bucket) ... " +large_data=$(head -c 10000 /dev/zero | tr '\0' 'A') +actual_status=$(curl -s -o /dev/null -w "%{http_code}" -X POST -d "$large_data" "$BASEURL/") +if [ "$actual_status" = "200" ]; then + echo -e "${GREEN}PASS${NC} (got $actual_status)" + PASSED=$((PASSED + 1)) +else + echo -e "${RED}FAIL${NC} (expected 200, got $actual_status)" + FAILED=$((FAILED + 1)) +fi + +# Test 6: Large POST with malicious content (should be blocked, tests our fix) +echo -n "Testing: Large POST with evil content ... " +large_evil_data="A$(head -c 9000 /dev/zero | tr '\0' 'A')malicious" +actual_status=$(curl -s -o /dev/null -w "%{http_code}" -X POST -d "$large_evil_data" "$BASEURL/") +if [ "$actual_status" = "403" ]; then + echo -e "${GREEN}PASS${NC} (got $actual_status - rule fired correctly on multi-bucket body)" + PASSED=$((PASSED + 1)) +else + echo -e "${RED}FAIL${NC} (expected 403, got $actual_status - this tests the request body processing fix!)" + FAILED=$((FAILED + 1)) +fi + +echo "" +echo "======================================" +echo "Test Results" +echo "======================================" +echo -e "Passed: ${GREEN}$PASSED${NC}" +echo -e "Failed: ${RED}$FAILED${NC}" +echo "" + +if [ $FAILED -eq 0 ]; then + echo -e "${GREEN}All tests passed!${NC}" + echo "" + echo "Key fixes verified:" + echo " ✓ Request body processing (rules fire once, not per bucket)" + echo " ✓ Status codes work correctly (403 is returned)" + echo " ✓ Multi-bucket POST requests processed correctly" + exit 0 +else + echo -e "${RED}Some tests failed!${NC}" + echo "" + echo "Check logs:" + echo " docker logs modsec3-apache-test" + echo " cat logs/error.log" + exit 1 +fi diff --git a/tools/soak.sh b/tools/soak.sh new file mode 100755 index 0000000..cbea384 --- /dev/null +++ b/tools/soak.sh @@ -0,0 +1,265 @@ +#!/usr/bin/env bash +# +# Sustained mixed-load soak for the ModSecurity Apache connector. Drives a +# real httpd (optionally under valgrind memcheck or helgrind) with +# concurrent benign AND attack-shaped requests for a fixed duration, while +# periodically triggering a graceful restart (SIGUSR1) -- the exact +# operation known to leak memory (see docs/TODO.md / issue #82) -- then +# asserts the server survived cleanly: no valgrind/helgrind error, no +# crash, no leak, no error-log [alert]/[emerg]. +# +# The traffic mix exercises the WAF decision path in both directions -- +# benign requests that must pass (200) and attack requests the in-config +# SecRules must block (403) -- so the transaction lifecycle (create/ +# process/destroy), request body buffering, and response body inspection +# all run under the checker every iteration, across many graceful restarts. +# +# httpd is run with -DFOREGROUND (like the module's own start.sh) so the +# worker/event MPM forks child processes and threads exactly as in +# production; valgrind is invoked with --trace-children=yes so those +# forked children -- where request handling and the module hooks actually +# run -- are instrumented too, not just the master process. +# +# Usage: +# tools/soak.sh