From 31a4ac1f03f2ba1caf1967871d8807bf9937aaa3 Mon Sep 17 00:00:00 2001 From: Josh Sokol Date: Sun, 20 Sep 2026 13:37:07 -0500 Subject: [PATCH 1/3] Generate simplerisk-minimal TLS cert at runtime, drop baked-in CA (HackerOne #3764027) The Dockerfile generated a CA key/cert and Apache TLS key/cert at build time, baked both into the published image layer, and installed the CA into the container's system trust store. Anyone who pulled the image could extract the CA private key and forge certs for any hostname the container's own outbound HTTPS calls (e.g. the schema fetch in entrypoint.sh) would then trust. Removes the custom CA entirely -- unnecessary, since the non-minimal simplerisk image self-signs its Apache cert directly without one -- and moves self-signed cert generation from build time to container startup, so each deployment gets its own key instead of a key shared by every pull of the same tag. Co-Authored-By: Claude Sonnet 5 --- simplerisk-minimal/Dockerfile | 21 ++++++-------- simplerisk-minimal/common/entrypoint.sh | 29 +++++++++++++++++++ .../apache2/sites-enabled/default-ssl.conf | 1 - simplerisk-minimal/generate_dockerfile.sh | 21 ++++++-------- 4 files changed, 47 insertions(+), 25 deletions(-) diff --git a/simplerisk-minimal/Dockerfile b/simplerisk-minimal/Dockerfile index a1082fa..30f1e36 100644 --- a/simplerisk-minimal/Dockerfile +++ b/simplerisk-minimal/Dockerfile @@ -99,18 +99,15 @@ RUN echo 'upload_max_filesize = 5M' >> /usr/local/etc/php/conf.d/docker-php-uplo echo 'log_errors = On' >> /usr/local/etc/php/conf.d/docker-php-error_logging.ini && \ echo 'error_log = /dev/stderr' >> /usr/local/etc/php/conf.d/docker-php-error_logging.ini && \ echo 'display_errors = Off' >> /usr/local/etc/php/conf.d/docker-php-error_logging.ini && \ -# Create SSL Certificates for Apache SSL - mkdir -p /etc/apache2/ssl/ca /etc/apache2/ssl/simplerisk && \ -# Generate CA - openssl genrsa -out /etc/apache2/ssl/ca/ca.key 4096 && \ - openssl req -x509 -new -nodes -key /etc/apache2/ssl/ca/ca.key -sha256 -days 3650 -out /etc/apache2/ssl/ca/ca.crt -subj "/CN=SimpleRisk CA" && \ -# Generate certs - openssl genrsa -out /etc/apache2/ssl/simplerisk/simplerisk.key 2048 && \ - openssl req -new -key /etc/apache2/ssl/simplerisk/simplerisk.key -out /etc/apache2/ssl/simplerisk/simplerisk.csr -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,DNS:simplerisk,IP:127.0.0.1,IP:0.0.0.0" && \ - openssl x509 -req -days 365 -in /etc/apache2/ssl/simplerisk/simplerisk.csr -CA /etc/apache2/ssl/ca/ca.crt -CAkey /etc/apache2/ssl/ca/ca.key -CAcreateserial -out /etc/apache2/ssl/simplerisk/simplerisk.crt -copy_extensions copyall && \ - cp /etc/apache2/ssl/ca/ca.crt /usr/local/share/ca-certificates/simplerisk.crt && \ - chmod 644 /usr/local/share/ca-certificates/simplerisk.crt && \ - update-ca-certificates && \ +# SSL certificate directory for Apache. The actual key pair is generated at +# container startup by entrypoint.sh (see set_ssl_certificate), not here at +# build time: a key baked into this RUN would be identical in every pulled +# copy of the image and, combined with registering it as a trusted CA, would +# let anyone who pulls the image forge certs the container trusts (HackerOne +# #3764027). No custom CA is created or installed into the system trust +# store -- Apache's cert is self-signed directly, matching the simplerisk +# (non-minimal) image. + mkdir -p /etc/apache2/ssl/simplerisk && \ # Activate Apache modules a2enmod headers rewrite ssl && \ a2enconf security && \ diff --git a/simplerisk-minimal/common/entrypoint.sh b/simplerisk-minimal/common/entrypoint.sh index ee6c210..cf43d40 100644 --- a/simplerisk-minimal/common/entrypoint.sh +++ b/simplerisk-minimal/common/entrypoint.sh @@ -202,6 +202,33 @@ set_csrf_secret(){ [ -n "${SIMPLERISK_CSRF_SECRET:-}" ] && echo "" > "$CSRF_SECRET_PATH"; } +set_ssl_certificate(){ + # Generate Apache's self-signed TLS key pair at container startup rather + # than at image build time. A key baked into the image (the previous + # behavior) is identical in every copy of the image anyone pulls, and was + # additionally being registered as a trusted CA in the container's system + # trust store -- anyone who extracted that key could forge certs the + # container's own outbound curl calls would accept (HackerOne #3764027). + # Generating here means each deployment gets its own key, and idempotence + # (skip if already present) means an existing cert persisted in the + # /etc/apache2/ssl volume survives container restarts/recreation. + local ssl_dir='/etc/apache2/ssl/simplerisk' + local key="$ssl_dir/simplerisk.key" + local crt="$ssl_dir/simplerisk.crt" + + if [ -f "$key" ] && [ -f "$crt" ]; then + return 0 + fi + + print_log "ssl_setup:info" "No Apache TLS certificate found; generating a self-signed one for this container." + exec_cmd "mkdir -p $ssl_dir" "Failed to create SSL certificate directory. Exiting." + # basicConstraints=CA:FALSE is explicit because openssl's own default + # x509 extensions otherwise mark a self-signed leaf cert as CA:TRUE, + # which Apache logs a warning about on every start. + exec_cmd "openssl req -x509 -newkey rsa:2048 -nodes -keyout $key -out $crt -days 365 -subj '/CN=localhost' -addext 'subjectAltName=DNS:localhost,DNS:simplerisk,IP:127.0.0.1,IP:0.0.0.0' -addext 'basicConstraints=critical,CA:FALSE' -addext 'keyUsage=critical,digitalSignature,keyEncipherment' -addext 'extendedKeyUsage=serverAuth'" "Failed to generate self-signed TLS certificate. Exiting." + exec_cmd "chmod 600 $key" "Failed to set permissions on TLS private key. Exiting." +} + set_cron(){ # If SIMPLERISK_CRON_SETUP was passed and it is set to disabled if [[ -n "${SIMPLERISK_CRON_SETUP:-}" && "${SIMPLERISK_CRON_SETUP:-}" = disabled* ]]; then @@ -443,6 +470,8 @@ unset_variables() { } _main() { + set_ssl_certificate + # Detect whether the operator has opted into Docker-managed config # provisioning. If no DB env vars are set, leave config.php absent so # SimpleRisk's web installer runs on first request. diff --git a/simplerisk-minimal/common/etc/apache2/sites-enabled/default-ssl.conf b/simplerisk-minimal/common/etc/apache2/sites-enabled/default-ssl.conf index 0b55c84..45dbc28 100644 --- a/simplerisk-minimal/common/etc/apache2/sites-enabled/default-ssl.conf +++ b/simplerisk-minimal/common/etc/apache2/sites-enabled/default-ssl.conf @@ -11,7 +11,6 @@ SSLStrictSNIVHostCheck Off Options -Indexes SSLEngine on - SSLCACertificateFile /etc/apache2/ssl/ca/ca.crt SSLCertificateFile /etc/apache2/ssl/simplerisk/simplerisk.crt SSLCertificateKeyFile /etc/apache2/ssl/simplerisk/simplerisk.key SSLProtocol -all +TLSv1.2 +TLSv1.3 diff --git a/simplerisk-minimal/generate_dockerfile.sh b/simplerisk-minimal/generate_dockerfile.sh index 53ba282..634b366 100755 --- a/simplerisk-minimal/generate_dockerfile.sh +++ b/simplerisk-minimal/generate_dockerfile.sh @@ -144,18 +144,15 @@ RUN echo 'upload_max_filesize = 5M' >> /usr/local/etc/php/conf.d/docker-php-uplo echo 'log_errors = On' >> /usr/local/etc/php/conf.d/docker-php-error_logging.ini && \\ echo 'error_log = /dev/stderr' >> /usr/local/etc/php/conf.d/docker-php-error_logging.ini && \\ echo 'display_errors = Off' >> /usr/local/etc/php/conf.d/docker-php-error_logging.ini && \\ -# Create SSL Certificates for Apache SSL - mkdir -p /etc/apache2/ssl/ca /etc/apache2/ssl/simplerisk && \\ -# Generate CA - openssl genrsa -out /etc/apache2/ssl/ca/ca.key 4096 && \\ - openssl req -x509 -new -nodes -key /etc/apache2/ssl/ca/ca.key -sha256 -days 3650 -out /etc/apache2/ssl/ca/ca.crt -subj "/CN=SimpleRisk CA" && \\ -# Generate certs - openssl genrsa -out /etc/apache2/ssl/simplerisk/simplerisk.key 2048 && \\ - openssl req -new -key /etc/apache2/ssl/simplerisk/simplerisk.key -out /etc/apache2/ssl/simplerisk/simplerisk.csr -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,DNS:simplerisk,IP:127.0.0.1,IP:0.0.0.0" && \\ - openssl x509 -req -days 365 -in /etc/apache2/ssl/simplerisk/simplerisk.csr -CA /etc/apache2/ssl/ca/ca.crt -CAkey /etc/apache2/ssl/ca/ca.key -CAcreateserial -out /etc/apache2/ssl/simplerisk/simplerisk.crt -copy_extensions copyall && \\ - cp /etc/apache2/ssl/ca/ca.crt /usr/local/share/ca-certificates/simplerisk.crt && \\ - chmod 644 /usr/local/share/ca-certificates/simplerisk.crt && \\ - update-ca-certificates && \\ +# SSL certificate directory for Apache. The actual key pair is generated at +# container startup by entrypoint.sh (see set_ssl_certificate), not here at +# build time: a key baked into this RUN would be identical in every pulled +# copy of the image and, combined with registering it as a trusted CA, would +# let anyone who pulls the image forge certs the container trusts (HackerOne +# #3764027). No custom CA is created or installed into the system trust +# store -- Apache's cert is self-signed directly, matching the simplerisk +# (non-minimal) image. + mkdir -p /etc/apache2/ssl/simplerisk && \\ # Activate Apache modules a2enmod headers rewrite ssl && \\ a2enconf security && \\ From d6860d5ef5cddb7f82f150290b099d1a2956fd00 Mon Sep 17 00:00:00 2001 From: Josh Sokol Date: Sun, 20 Sep 2026 13:37:12 -0500 Subject: [PATCH 2/3] Run simplerisk cron.php as www-data, not root /var/www/simplerisk (including cron/cron.php) is chowned to www-data, but the cron.d entry executed it as root every minute. Any web-tier compromise that gets code execution as www-data could overwrite cron.php and escalate to root on the next cron tick -- the same CWE-732 privilege-boundary crossing reported and fixed upstream in simplerisk/setup-scripts as HackerOne #3761952, just baked into this image's Dockerfile instead of the bare-metal installer script. cron.php only runs application-level jobs that already execute as www-data under Apache, so it doesn't need root. Co-Authored-By: Claude Sonnet 5 --- simplerisk/Dockerfile | 8 +++++++- simplerisk/generate_dockerfile.sh | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/simplerisk/Dockerfile b/simplerisk/Dockerfile index ca867ea..55a7f51 100644 --- a/simplerisk/Dockerfile +++ b/simplerisk/Dockerfile @@ -110,7 +110,13 @@ RUN chown -R www-data: /var/www/simplerisk RUN chown -R www-data: /var/log/simplerisk # Setting up cronjob -RUN echo "* * * * * root /usr/bin/php -f /var/www/simplerisk/cron/cron.php > /dev/null 2>&1" >> /etc/cron.d/simplerisk-cron && \ +# Runs as www-data, not root: /var/www/simplerisk (including cron.php) is +# chowned to www-data above, so a root cron entry over a www-data-writable +# file would let any web-tier compromise escalate to root on the next tick +# (the same class of bug as CWE-732 / HackerOne #3761952). cron.php only runs +# application-level jobs that already execute as www-data under Apache, so it +# doesn't need root. +RUN echo "* * * * * www-data /usr/bin/php -f /var/www/simplerisk/cron/cron.php > /dev/null 2>&1" >> /etc/cron.d/simplerisk-cron && \ chmod 0644 /etc/cron.d/simplerisk-cron RUN echo "0 0 * * * root /usr/sbin/logrotate /etc/logrotate.d/simplerisk.conf > /dev/null 2>&1" >> /etc/cron.d/logrotate-cron && \ chmod 0644 /etc/cron.d/logrotate-cron diff --git a/simplerisk/generate_dockerfile.sh b/simplerisk/generate_dockerfile.sh index 10e347a..4cadb84 100755 --- a/simplerisk/generate_dockerfile.sh +++ b/simplerisk/generate_dockerfile.sh @@ -159,7 +159,13 @@ RUN chown -R www-data: /var/www/simplerisk RUN chown -R www-data: /var/log/simplerisk # Setting up cronjob -RUN echo "* * * * * root /usr/bin/php -f /var/www/simplerisk/cron/cron.php > /dev/null 2>&1" >> /etc/cron.d/simplerisk-cron && \\ +# Runs as www-data, not root: /var/www/simplerisk (including cron.php) is +# chowned to www-data above, so a root cron entry over a www-data-writable +# file would let any web-tier compromise escalate to root on the next tick +# (the same class of bug as CWE-732 / HackerOne #3761952). cron.php only runs +# application-level jobs that already execute as www-data under Apache, so it +# doesn't need root. +RUN echo "* * * * * www-data /usr/bin/php -f /var/www/simplerisk/cron/cron.php > /dev/null 2>&1" >> /etc/cron.d/simplerisk-cron && \\ chmod 0644 /etc/cron.d/simplerisk-cron RUN echo "0 0 * * * root /usr/sbin/logrotate /etc/logrotate.d/simplerisk.conf > /dev/null 2>&1" >> /etc/cron.d/logrotate-cron && \\ chmod 0644 /etc/cron.d/logrotate-cron From 24528f84b3678376af23e8f9bb0b26671eb11548 Mon Sep 17 00:00:00 2001 From: Josh Sokol Date: Sun, 20 Sep 2026 13:51:45 -0500 Subject: [PATCH 3/3] grype: ignore newly published curl binary-classifier false positives CVE-2026-19931 and CVE-2026-18924 are the same pre-existing Grype false positive already documented above (binary classifier reads the PHP interpreter's embedded version string out of curl.so and reports it as curl's own version), just newly published CVE IDs against that same fictitious version. Verified via `grype -o json`: the only match is /usr/local/lib/php/extensions/.../curl.so; the real Debian curl package is a separate match Debian's tracker marks "wont-fix", which --only-fixed already excludes on its own. This was failing container-validation.yml on all three simplerisk-minimal PHP variants. Co-Authored-By: Claude Sonnet 5 --- .grype.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.grype.yaml b/.grype.yaml index 23614b6..04b4903 100644 --- a/.grype.yaml +++ b/.grype.yaml @@ -22,3 +22,18 @@ ignore: package: name: curl type: binary + # Same binary-classifier false positive as above, newly published CVE IDs. + # Verified against simplerisk-minimal (php 8.3/8.4/8.5): the only "curl" + # match is /usr/local/lib/php/extensions/.../curl.so reporting the PHP + # version (e.g. 8.5.10) as its own. The real Debian curl package + # (8.14.1-2+deb13u5) is also affected but Debian's tracker marks both IDs + # "wont-fix", so --only-fixed already excludes that (legitimate) match on + # its own; only the misclassified binary match needs ignoring here. + - vulnerability: CVE-2026-19931 + package: + name: curl + type: binary + - vulnerability: CVE-2026-18924 + package: + name: curl + type: binary