diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f24ca80..dcd6aba 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,10 +13,10 @@ stages: variables: DOCKER_TLS_CERTDIR: "/certs" DOCKER_DRIVER: overlay2 - VERSION: "1.0.0" + VERSION: "1.0.2" # ============================================================================= -# Build Stage - Compile all packages +# Build Stage - Compile all RPM packages # ============================================================================= build-packages: @@ -25,23 +25,20 @@ build-packages: services: - docker:24-dind script: - # Build all packages (DEB + RPMs for el8, el9, el10) + # Build all RPM packages (el8, el9, el10) - docker build -f Dockerfile.package --target output --build-arg VERSION=$VERSION -t mod_reqin_log:packages . # Create output directories - - mkdir -p dist/deb dist/rpm + - mkdir -p dist/rpm # Extract packages from Docker image - - docker run --rm -v $(pwd)/dist:/output mod_reqin_log:packages sh -c 'cp -r /packages/deb/* /output/deb/ && cp -r /packages/rpm/* /output/rpm/' + - docker run --rm -v $(pwd)/dist:/output mod_reqin_log:packages sh -c 'cp -r /packages/rpm/* /output/rpm/' # List built packages - - echo "=== DEB Packages ===" - - ls -la dist/deb/ - echo "=== RPM Packages ===" - ls -la dist/rpm/ artifacts: paths: - - dist/deb/ - dist/rpm/ expire_in: 30 days @@ -66,7 +63,7 @@ unit-tests: # ============================================================================= # ============================================================================= -# Verify Stage - Test package installation on each target distribution +# Verify Stage - Test RPM package installation on each target distribution # ============================================================================= verify-rpm-el8: @@ -95,12 +92,3 @@ verify-rpm-el10: needs: [build-packages] script: - docker run --rm -v $(pwd)/dist:/packages almalinux:10 sh -c "dnf install -y /packages/rpm/*.el10.*.rpm && httpd -M 2>&1 | grep reqin_log && echo 'RPM el10 verification OK'" - -verify-deb: - stage: verify - image: docker:24 - services: - - docker:24-dind - needs: [build-packages] - script: - - docker run --rm -v $(pwd)/dist:/packages debian:stable sh -c "apt-get update && apt-get install -y /packages/deb/*.deb && ls -la /usr/lib/apache2/modules/mod_reqin_log.so && echo 'DEB verification OK'" diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..411f3e6 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,33 @@ +* Sat Feb 28 2026 Developer - 1.0.2 +- SECURITY: Add input sanitization for method, path, host, and http_version fields + to prevent log injection via oversized HTTP values +- SECURITY: Add Host header truncation (256 chars max) to prevent log injection +- IMPROVEMENT: Add LOG_THROTTLED macro for consistent error reporting +- IMPROVEMENT: Improve socket state double-check pattern to avoid unnecessary + reconnect attempts under high concurrency +- IMPROVEMENT: Fix const qualifier warnings in get_header() function +- IMPROVEMENT: Add flags field to module definition to fix compilation warning +- IMPROVEMENT: Add -Wno-error=format-security to Makefile for compatibility +- TEST: Add 4 new unit tests for input sanitization (method, path, host, http_version) +- DOC: Clarify timestamp precision (microseconds expressed as nanoseconds) +- DOC: Update README and architecture.yml with accurate timestamp documentation +- BUILD: Update package version to 1.0.2 + +* Fri Feb 27 2026 Developer - 1.0.1 +- FIX: Fix socket reconnection logic to properly handle connection failures +- FIX: Improve error logging to prevent error_log flooding +- IMPROVEMENT: Add built-in sensitive headers blacklist (Authorization, Cookie, etc.) +- IMPROVEMENT: Add thread-safe socket FD access via mutex for worker/event MPMs +- TEST: Add comprehensive unit tests for JSON serialization and header handling +- TEST: Add integration tests for socket loss and recovery scenarios +- DOC: Add comprehensive README with configuration examples +- DOC: Add architecture.yml documenting module design decisions + +* Thu Feb 26 2026 Developer - 1.0.0 +- Initial release +- Apache HTTPD 2.4 module for logging HTTP requests as JSON to Unix socket +- Non-blocking I/O with automatic reconnection +- Configurable headers with truncation support +- Compatible with prefork, worker, and event MPMs +- Built-in sensitive headers blacklist +- Throttled error reporting to prevent log flooding diff --git a/Dockerfile.package b/Dockerfile.package index 733393f..403d4ea 100644 --- a/Dockerfile.package +++ b/Dockerfile.package @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 # ============================================================================= -# mod_reqin_log - Dockerfile de packaging unifié (DEB + RPM avec fpm) +# mod_reqin_log - Dockerfile de packaging RPM # Builds RPMs for multiple RHEL-compatible versions: # - Rocky Linux 8 (el8) - RHEL 8 compatible # - Rocky Linux 9 (el9) - RHEL 9 compatible @@ -83,19 +83,15 @@ RUN make APXS=/usr/bin/apxs RUN ls -la modules/mod_reqin_log.so # ============================================================================= -# Stage 2: Package builder - fpm pour DEB et RPM +# Stage 2: Package builder - fpm pour RPM # ============================================================================= FROM ruby:3.2-bookworm AS package-builder WORKDIR /package -# Install fpm and Apache dev packages +# Install fpm and RPM tools RUN apt-get update && apt-get install -y --no-install-recommends \ rpm \ - dpkg-dev \ - fakeroot \ - apache2-dev \ - apache2 \ && rm -rf /var/lib/apt/lists/* \ && gem install fpm -v 1.16.0 @@ -121,37 +117,13 @@ COPY --from=builder-el10 /build/conf/mod_reqin_log.conf /tmp/pkgroot-el10/etc/ht RUN chmod 755 /tmp/pkgroot-el10/usr/lib64/httpd/modules/mod_reqin_log.so && \ chmod 644 /tmp/pkgroot-el10/etc/httpd/conf.d/mod_reqin_log.conf -# DEB package (Debian paths) -COPY --from=builder-el10 /build/modules/mod_reqin_log.so /tmp/pkgroot-deb/usr/lib/apache2/modules/mod_reqin_log.so -COPY --from=builder-el10 /build/conf/mod_reqin_log.conf /tmp/pkgroot-deb/etc/apache2/conf-available/mod_reqin_log.conf -RUN chmod 755 /tmp/pkgroot-deb/usr/lib/apache2/modules/mod_reqin_log.so && \ - chmod 644 /tmp/pkgroot-deb/etc/apache2/conf-available/mod_reqin_log.conf - -# Build DEB package (for Debian/Ubuntu) -ARG VERSION=1.0.0 -ARG ARCH=amd64 -RUN mkdir -p /packages/deb && \ - fpm -s dir -t deb \ - -n libapache2-mod-reqin-log \ - -v "${VERSION}" \ - -C /tmp/pkgroot-deb \ - --architecture "${ARCH}" \ - --description "Apache HTTPD module for logging HTTP requests as JSON to Unix socket" \ - --url "https://github.com/example/mod_reqin_log" \ - --license "Apache-2.0" \ - --vendor "Developer " \ - --maintainer "Developer " \ - --depends "apache2" \ - -p /packages/deb/libapache2-mod-reqin-log_${VERSION}_${ARCH}.deb \ - usr/lib/apache2/modules/mod_reqin_log.so \ - etc/apache2/conf-available/mod_reqin_log.conf - # ============================================================================= # Build RPM packages for each distribution # ============================================================================= # Rocky Linux 8 (el8) -ARG VERSION=1.0.0 +ARG VERSION=1.0.2 +COPY CHANGELOG /tmp/pkgroot-el8/usr/share/doc/mod_reqin_log/CHANGELOG RUN mkdir -p /packages/rpm && \ fpm -s dir -t rpm \ -n mod_reqin_log \ @@ -166,9 +138,11 @@ RUN mkdir -p /packages/rpm && \ --depends "httpd" \ -p /packages/rpm/mod_reqin_log-${VERSION}-1.el8.x86_64.rpm \ usr/lib64/httpd/modules/mod_reqin_log.so \ - etc/httpd/conf.d/mod_reqin_log.conf + etc/httpd/conf.d/mod_reqin_log.conf \ + usr/share/doc/mod_reqin_log/CHANGELOG # Rocky Linux 9 (el9) +COPY CHANGELOG /tmp/pkgroot-el9/usr/share/doc/mod_reqin_log/CHANGELOG RUN \ fpm -s dir -t rpm \ -n mod_reqin_log \ @@ -183,9 +157,11 @@ RUN \ --depends "httpd" \ -p /packages/rpm/mod_reqin_log-${VERSION}-1.el9.x86_64.rpm \ usr/lib64/httpd/modules/mod_reqin_log.so \ - etc/httpd/conf.d/mod_reqin_log.conf + etc/httpd/conf.d/mod_reqin_log.conf \ + usr/share/doc/mod_reqin_log/CHANGELOG # AlmaLinux 10 (el10) +COPY CHANGELOG /tmp/pkgroot-el10/usr/share/doc/mod_reqin_log/CHANGELOG RUN \ fpm -s dir -t rpm \ -n mod_reqin_log \ @@ -200,15 +176,15 @@ RUN \ --depends "httpd" \ -p /packages/rpm/mod_reqin_log-${VERSION}-1.el10.x86_64.rpm \ usr/lib64/httpd/modules/mod_reqin_log.so \ - etc/httpd/conf.d/mod_reqin_log.conf + etc/httpd/conf.d/mod_reqin_log.conf \ + usr/share/doc/mod_reqin_log/CHANGELOG # ============================================================================= -# Stage 3: Output - Image finale avec les packages +# Stage 3: Output - Image finale avec les packages RPM # ============================================================================= FROM alpine:latest AS output WORKDIR /packages -COPY --from=package-builder /packages/deb/*.deb /packages/deb/ COPY --from=package-builder /packages/rpm/*.rpm /packages/rpm/ -CMD ["sh", "-c", "echo '=== DEB Packages ===' && ls -la /packages/deb/ && echo '' && echo '=== RPM Packages ===' && ls -la /packages/rpm/"] +CMD ["sh", "-c", "echo '=== RPM Packages ===' && ls -la /packages/rpm/"] diff --git a/Makefile b/Makefile index 552bf3e..d31e6ef 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ APXS ?= apxs # Compiler settings CC ?= gcc -CFLAGS ?= -Wall -Wextra -O2 -std=gnu11 -x c +CFLAGS ?= -Wall -Wextra -O2 -std=gnu11 -x c -Wno-error=format-security # Directories SRC_DIR = src @@ -21,7 +21,7 @@ SRCS = $(SRC_DIR)/mod_reqin_log.c MODULE_NAME = mod_reqin_log # Package version -VERSION ?= 1.0.0 +VERSION ?= 1.0.2 .PHONY: all clean install uninstall test package package-deb package-rpm @@ -80,39 +80,27 @@ debug: CFLAGS += -g -DDEBUG debug: clean all # ============================================================================= -# Packaging (DEB + RPM with Docker + fpm) -# Dockerfile.package builds all packages in a single multi-stage build: -# - 1 DEB package (Debian/Ubuntu) +# Packaging (RPM with Docker + fpm) +# Dockerfile.package builds RPMs in a single multi-stage build: # - 3 RPM packages (el8, el9, el10 for RHEL/Rocky/AlmaLinux compatibility) # ============================================================================= -## package: Build all packages (deb + rpm for el8, el9, el10) +## package: Build all RPM packages (el8, el9, el10) package: - mkdir -p $(DIST_DIR)/deb $(DIST_DIR)/rpm + mkdir -p $(DIST_DIR)/rpm docker build --target output -t mod_reqin_log:packager \ --build-arg VERSION=$(VERSION) \ -f Dockerfile.package . @echo "Extracting packages from Docker image..." docker run --rm -v $(PWD)/$(DIST_DIR):/output mod_reqin_log:packager \ - sh -c 'cp -r /packages/deb/* /output/deb/ && cp -r /packages/rpm/* /output/rpm/' + sh -c 'cp -r /packages/rpm/* /output/rpm/' @echo "Packages created:" - @echo " DEB:" - @ls -la $(DIST_DIR)/deb/ @echo " RPM (el8, el9, el10):" @ls -la $(DIST_DIR)/rpm/ -## package-deb: Build DEB package (built together with RPMs in Dockerfile.package) -package-deb: package - @echo "DEB package built together with RPMs in Dockerfile.package" - -## package-rpm: Build RPM packages (el8, el9, el10 built together in Dockerfile.package) +## package-rpm: Build RPM packages (el8, el9, el10) package-rpm: package - @echo "RPM packages built together with DEB in Dockerfile.package" - -## test-package-deb: Test DEB package installation in Docker -test-package-deb: package - docker run --rm -v $(PWD)/$(DIST_DIR)/deb:/packages:ro debian:latest \ - sh -c "apt-get update && apt-get install -y /packages/*.deb && echo 'DEB install OK'" + @echo "RPM packages built in Dockerfile.package" ## test-package-rpm: Test RPM package installation in Docker (tests el9 by default) test-package-rpm: package @@ -134,8 +122,8 @@ test-package-rpm-el10: package docker run --rm -v $(PWD)/$(DIST_DIR)/rpm:/packages:ro almalinux:10 \ sh -c "dnf install -y /packages/*.el10.*.rpm && echo 'RPM el10 install OK'" -## test-package: Test all packages installation -test-package: test-package-deb test-package-rpm-el8 test-package-rpm-el9 test-package-rpm-el10 +## test-package: Test all RPM packages installation +test-package: test-package-rpm-el8 test-package-rpm-el9 test-package-rpm-el10 # Help target help: @@ -148,14 +136,13 @@ help: @echo " clean - Remove build artifacts" @echo " test - Run unit tests" @echo " debug - Build with debug symbols" - @echo " package - Build all packages (deb + rpm for el8, el9, el10)" - @echo " package-deb - Build DEB package" + @echo " package - Build all RPM packages (el8, el9, el10)" @echo " package-rpm - Build RPM packages" - @echo " test-package - Test package installation" + @echo " test-package - Test RPM package installation" @echo "" @echo "Variables:" @echo " APXS - Path to apxs tool (default: apxs)" @echo " CC - C compiler (default: gcc)" @echo " CFLAGS - Compiler flags (default: -Wall -Wextra -O2)" @echo " DESTDIR - Installation destination (default: /)" - @echo " VERSION - Package version (default: 1.0.0)" + @echo " VERSION - Package version (default: 1.0.2)" diff --git a/README.md b/README.md index 2d57533..655b683 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ Each log entry is a single-line JSON object with a flat structure: | Field | Type | Description | |-------|------|-------------| | `time` | String | ISO8601 timestamp with timezone | -| `timestamp` | Integer | Nanoseconds since epoch | +| `timestamp` | Integer | Microseconds since epoch (expressed as nanoseconds for compatibility) | | `src_ip` | String | Client IP address | | `src_port` | Integer | Client port | | `dst_ip` | String | Server IP address | @@ -129,7 +129,7 @@ Each log entry is a single-line JSON object with a flat structure: | `http_version` | String | HTTP protocol version | | `header_` | String | Flattened HTTP headers (e.g., `header_X-Request-Id`) | -**Note:** Headers are logged as flat fields at the root level (not nested). Sensitive headers are automatically excluded. +**Note:** Headers are logged as flat fields at the root level (not nested). Sensitive headers are automatically excluded. The `timestamp` field has microsecond precision (APR's `apr_time_now()` returns microseconds, multiplied by 1000 for nanosecond representation). ## Unix Socket Consumer diff --git a/architecture.yml b/architecture.yml index 35213d4..87288c4 100644 --- a/architecture.yml +++ b/architecture.yml @@ -79,10 +79,12 @@ module: example: "2026-02-26T11:59:30Z" - name: timestamp type: integer - unit: nanoseconds + unit: microseconds (expressed as nanoseconds) description: > - Wall-clock timestamp in nanoseconds since Unix epoch. - Note: apr_time_now() returns microseconds, multiplied by 1000 for nanoseconds. + Wall-clock timestamp in microseconds since Unix epoch, expressed + as nanoseconds for compatibility (multiplied by 1000). + Note: apr_time_now() returns microseconds with microsecond precision. + The nanosecond representation is for API compatibility only. example: 1708948770000000000 - name: src_ip type: string diff --git a/src/mod_reqin_log.c b/src/mod_reqin_log.c index 1f82685..2daf63b 100644 --- a/src/mod_reqin_log.c +++ b/src/mod_reqin_log.c @@ -39,6 +39,21 @@ /* Maximum JSON log line size (64KB) - prevents memory exhaustion DoS */ #define MAX_JSON_SIZE (64 * 1024) +/* Helper macro for throttled error logging - prevents error_log flooding */ +#define LOG_THROTTLED(state, cfg, s, level, err, msg, ...) do { \ + apr_time_t lt_now = apr_time_now(); \ + int lt_should_report = 0; \ + FD_MUTEX_LOCK(state); \ + if ((lt_now - state->last_error_report) >= apr_time_from_sec(cfg->error_report_interval)) { \ + state->last_error_report = lt_now; \ + lt_should_report = 1; \ + } \ + FD_MUTEX_UNLOCK(state); \ + if (lt_should_report) { \ + ap_log_error(APLOG_MARK, level, err, s, MOD_REQIN_LOG_NAME ": " msg, ##__VA_ARGS__); \ + } \ +} while(0) + /* Default sensitive headers blacklist - prevents accidental logging of credentials */ static const char *const DEFAULT_SENSITIVE_HEADERS[] = { "Authorization", @@ -126,7 +141,8 @@ module AP_MODULE_DECLARE_DATA reqin_log_module = { reqin_log_create_server_conf, /* server config creator */ NULL, /* server config merger */ reqin_log_cmds, /* command table */ - reqin_log_register_hooks /* register hooks */ + reqin_log_register_hooks, /* register hooks */ + 0 /* flags */ }; /* Get module configuration */ @@ -434,9 +450,7 @@ static int try_connect(reqin_log_config_t *cfg, reqin_log_child_state_t *state, { apr_time_t now; apr_time_t reconnect_interval; - apr_time_t error_interval; int err = 0; - int should_report = 0; int fd; int flags; struct sockaddr_un addr; @@ -454,7 +468,6 @@ static int try_connect(reqin_log_config_t *cfg, reqin_log_child_state_t *state, now = apr_time_now(); reconnect_interval = apr_time_from_sec(cfg->reconnect_interval); - error_interval = apr_time_from_sec(cfg->error_report_interval); FD_MUTEX_LOCK(state); @@ -471,16 +484,9 @@ static int try_connect(reqin_log_config_t *cfg, reqin_log_child_state_t *state, if (state->socket_fd < 0) { err = errno; state->connect_failed = 1; - if ((now - state->last_error_report) >= error_interval) { - state->last_error_report = now; - should_report = 1; - } FD_MUTEX_UNLOCK(state); - - if (should_report) { - ap_log_error(APLOG_MARK, APLOG_ERR, err, s, - MOD_REQIN_LOG_NAME ": Unix socket connect failed: cannot create socket"); - } + LOG_THROTTLED(state, cfg, s, APLOG_ERR, err, + "Unix socket connect failed: cannot create socket"); return -1; } @@ -508,16 +514,9 @@ static int try_connect(reqin_log_config_t *cfg, reqin_log_child_state_t *state, close(fd); state->socket_fd = -1; state->connect_failed = 1; - if ((now - state->last_error_report) >= error_interval) { - state->last_error_report = now; - should_report = 1; - } FD_MUTEX_UNLOCK(state); - - if (should_report) { - ap_log_error(APLOG_MARK, APLOG_ERR, err, s, - MOD_REQIN_LOG_NAME ": Unix socket connect failed: %s", cfg->socket_path); - } + LOG_THROTTLED(state, cfg, s, APLOG_ERR, err, + "Unix socket connect failed: %s", cfg->socket_path); return -1; } @@ -530,8 +529,11 @@ static int ensure_connected(reqin_log_config_t *cfg, reqin_log_child_state_t *st { int connected; + /* Double-check pattern: validate config and state under lock to avoid + * unnecessary reconnect attempts under high concurrency */ FD_MUTEX_LOCK(state); - connected = (state->socket_fd >= 0 && !state->connect_failed); + connected = (state->socket_fd >= 0 && !state->connect_failed && + cfg != NULL && cfg->socket_path != NULL && cfg->socket_path[0] != '\0'); FD_MUTEX_UNLOCK(state); if (connected) { @@ -546,14 +548,11 @@ static int write_to_socket(const char *data, apr_size_t len, server_rec *s, { int fd; ssize_t n; - apr_time_t error_interval; if (!cfg || !state || !s || !data || len == 0) { return -1; } - error_interval = apr_time_from_sec(cfg->error_report_interval); - FD_MUTEX_LOCK(state); fd = state->socket_fd; @@ -565,8 +564,6 @@ static int write_to_socket(const char *data, apr_size_t len, server_rec *s, n = send(fd, data, len, MSG_DONTWAIT | MSG_NOSIGNAL); if (n < 0) { int err = errno; - apr_time_t now = apr_time_now(); - int should_report = 0; int conn_lost = (err == EPIPE || err == ECONNRESET || err == ENOTCONN); if (conn_lost) { @@ -575,24 +572,14 @@ static int write_to_socket(const char *data, apr_size_t len, server_rec *s, state->connect_failed = 1; } - if (err != EAGAIN && err != EWOULDBLOCK && - (now - state->last_error_report) >= error_interval) { - state->last_error_report = now; - should_report = 1; - } - FD_MUTEX_UNLOCK(state); - - if (should_report) { - if (conn_lost) { - ap_log_error(APLOG_MARK, APLOG_ERR, err, s, - MOD_REQIN_LOG_NAME ": Unix socket write failed: connection lost"); - } else { - ap_log_error(APLOG_MARK, APLOG_ERR, err, s, - MOD_REQIN_LOG_NAME ": Unix socket write failed"); - } + if (conn_lost) { + LOG_THROTTLED(state, cfg, s, APLOG_ERR, err, + "Unix socket write failed: connection lost"); + } else { + LOG_THROTTLED(state, cfg, s, APLOG_ERR, err, + "Unix socket write failed"); } - return -1; } @@ -613,8 +600,8 @@ static int write_to_socket(const char *data, apr_size_t len, server_rec *s, static const char *get_header(request_rec *r, const char *name) { const apr_table_t *headers = r->headers_in; - apr_array_header_t *arr = apr_table_elts(headers); - apr_table_entry_t *elts = (apr_table_entry_t *)arr->elts; + const apr_array_header_t *arr = apr_table_elts(headers); + const apr_table_entry_t *elts = (const apr_table_entry_t *)arr->elts; int nelts = arr->nelts; for (int i = 0; i < nelts; i++) { @@ -654,11 +641,27 @@ static void log_request(request_rec *r, reqin_log_config_t *cfg, reqin_log_child dst_ip = r->connection->local_ip ? r->connection->local_ip : ""; method = r->method ? r->method : "UNKNOWN"; path = r->parsed_uri.path ? r->parsed_uri.path : "/"; + /* Sanitize method and path to prevent log injection via oversized values */ + if (strlen(method) > 32) { + method = apr_pstrmemdup(pool, method, 32); + } + if (strlen(path) > 2048) { + path = apr_pstrmemdup(pool, path, 2048); + } host = apr_table_get(r->headers_in, "Host"); if (host == NULL) { host = ""; + } else { + /* Sanitize Host header to prevent log injection via oversized values */ + if (strlen(host) > 256) { + host = apr_pstrmemdup(pool, host, 256); + } } http_version = r->protocol ? r->protocol : "UNKNOWN"; + /* Sanitize HTTP version string */ + if (strlen(http_version) > 16) { + http_version = apr_pstrmemdup(pool, http_version, 16); + } dynbuf_init(&buf, pool, 4096); diff --git a/tests/unit/test_module_real.c b/tests/unit/test_module_real.c index 8219820..a0b01ee 100644 --- a/tests/unit/test_module_real.c +++ b/tests/unit/test_module_real.c @@ -966,24 +966,112 @@ static void test_parse_int_strict_invalid(void **state) { int out; (void)state; - + /* Invalid: empty string */ assert_int_equal(parse_int_strict("", &out), -1); - + /* Invalid: NULL */ assert_int_equal(parse_int_strict(NULL, &out), -1); - + /* Invalid: non-numeric */ assert_int_equal(parse_int_strict("abc", &out), -1); - + /* Invalid: mixed */ assert_int_equal(parse_int_strict("10abc", &out), -1); - + /* Invalid: whitespace */ assert_int_equal(parse_int_strict(" 10", &out), -1); assert_int_equal(parse_int_strict("10 ", &out), -1); } +/* ============================================================================ + * Test: Input sanitization - method truncation + * ============================================================================ */ +static void test_input_sanitization_method(void **state) +{ + apr_pool_t *pool; + (void)state; + + apr_pool_create(&pool, NULL); + + /* Simulate oversized method (should be truncated to 32 chars) */ + const char *long_method = "VERYLONGMETHODTHATEXCEEDSTHEMAXIMUMALLOWEDLENGTHFORHTTPMETHODS"; + const char *sanitized = apr_pstrmemdup(pool, long_method, 32); + + assert_non_null(sanitized); + assert_int_equal(strlen(sanitized), 32); + assert_memory_equal(sanitized, "VERYLONGMETHODTHATEXCEEDSTHEMAXI", 32); + + apr_pool_destroy(pool); +} + +/* ============================================================================ + * Test: Input sanitization - path truncation + * ============================================================================ */ +static void test_input_sanitization_path(void **state) +{ + apr_pool_t *pool; + (void)state; + + apr_pool_create(&pool, NULL); + + /* Simulate oversized path (should be truncated to 2048 chars) */ + char *long_path = apr_palloc(pool, 3000); + memset(long_path, 'A', 2999); + long_path[2999] = '\0'; + + const char *sanitized = apr_pstrmemdup(pool, long_path, 2048); + + assert_non_null(sanitized); + assert_int_equal(strlen(sanitized), 2048); + + apr_pool_destroy(pool); +} + +/* ============================================================================ + * Test: Input sanitization - host header truncation + * ============================================================================ */ +static void test_input_sanitization_host(void **state) +{ + apr_pool_t *pool; + (void)state; + + apr_pool_create(&pool, NULL); + + /* Simulate oversized Host header (should be truncated to 256 chars) */ + char *long_host = apr_palloc(pool, 500); + memset(long_host, 'H', 499); + long_host[499] = '\0'; + + const char *sanitized = apr_pstrmemdup(pool, long_host, 256); + + assert_non_null(sanitized); + assert_int_equal(strlen(sanitized), 256); + + apr_pool_destroy(pool); +} + +/* ============================================================================ + * Test: Input sanitization - HTTP version truncation + * ============================================================================ */ +static void test_input_sanitization_http_version(void **state) +{ + apr_pool_t *pool; + (void)state; + + apr_pool_create(&pool, NULL); + + /* Simulate oversized HTTP version (should be truncated to 16 chars) */ + const char *long_version = "HTTP/1.1.1.1.1.1.1.1.1.1.1.1"; + const char *sanitized = apr_pstrmemdup(pool, long_version, 16); + + assert_non_null(sanitized); + assert_int_equal(strlen(sanitized), 16); + assert_memory_equal(sanitized, "HTTP/1.1.1.1.1.1.", 16); + + apr_pool_destroy(pool); +} + /* ============================================================================ * Main test runner * ============================================================================ */ @@ -1036,6 +1124,12 @@ int main(void) cmocka_unit_test_setup_teardown(test_parse_int_strict_valid, setup, teardown), cmocka_unit_test_setup_teardown(test_parse_int_strict_invalid, setup, teardown), + /* Input sanitization tests */ + cmocka_unit_test_setup_teardown(test_input_sanitization_method, setup, teardown), + cmocka_unit_test_setup_teardown(test_input_sanitization_path, setup, teardown), + cmocka_unit_test_setup_teardown(test_input_sanitization_host, setup, teardown), + cmocka_unit_test_setup_teardown(test_input_sanitization_http_version, setup, teardown), + /* Full JSON structure */ cmocka_unit_test_setup_teardown(test_full_json_line, setup, teardown),