diff --git a/.github/workflows/Dockerfile.build-extension b/.github/workflows/Dockerfile.build-extension new file mode 100644 index 000000000..4b1232af7 --- /dev/null +++ b/.github/workflows/Dockerfile.build-extension @@ -0,0 +1,75 @@ +# syntax=docker/dockerfile:1.7 + +ARG BASE_IMAGE=ubuntu:20.04 +ARG PHP_VERSION=8.3 +ARG PHP_SRC_REF=PHP-${PHP_VERSION} + +FROM ${BASE_IMAGE} AS base +SHELL ["/bin/bash", "-eo", "pipefail", "-c"] + +ENV DEBIAN_FRONTEND=noninteractive \ + TZ=Etc/UTC \ + LC_ALL=C.UTF-8 \ + LANG=C.UTF-8 \ + LANGUAGE=C.UTF-8 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends tzdata ca-certificates git wget curl xz-utils \ + && ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime \ + && echo "${TZ}" > /etc/timezone \ + && dpkg-reconfigure -f noninteractive tzdata \ + && update-ca-certificates \ + && git config --global http.sslCAInfo /etc/ssl/certs/ca-certificates.crt \ + && git config --global http.sslVerify true \ + && rm -rf /var/lib/apt/lists/* + +# Builder: toolchain + dev libs +FROM base AS build-deps +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential autoconf bison re2c pkg-config \ + libxml2-dev libsqlite3-dev libcurl4-openssl-dev libssl-dev \ + libzip-dev libonig-dev libjpeg-dev libpng-dev libwebp-dev \ + libicu-dev libreadline-dev libxslt1-dev default-libmysqlclient-dev \ + wget tar \ + && rm -rf /var/lib/apt/lists/* + + +# Fetch php-src +FROM build-deps AS php-src +ARG PHP_SRC_REF +WORKDIR /usr/src +RUN git clone --depth 1 --branch "${PHP_SRC_REF}" https://github.com/php/php-src.git +WORKDIR /usr/src/php-src +RUN ./buildconf --force + + +# Build PHP +FROM php-src AS php-build +# Configure flags mirror your workflow; adjust as needed +RUN ./configure \ + --prefix=/usr/local \ + --with-config-file-path=/usr/local/lib \ + --with-config-file-scan-dir=/usr/local/etc/php/conf.d \ + --enable-mbstring \ + --enable-pcntl \ + --enable-intl \ + --with-curl \ + --with-mysqli \ + --with-openssl \ + --with-zlib \ + --with-zip \ + && make -j"$(nproc)" \ + && make install \ + && strip /usr/local/bin/php || true + + +FROM build-deps AS dev +COPY --from=php-build /usr/local /usr/local +# Sanity check and helpful defaults +RUN php -v && php -m | grep -E 'curl|mysqli' >/dev/null +ENV PATH="/usr/local/bin:${PATH}" + +RUN mkdir -p /usr/local/etc/php/conf.d +WORKDIR /work +CMD ["php", "-v"] + diff --git a/.github/workflows/Dockerfile.build-libs b/.github/workflows/Dockerfile.build-libs new file mode 100644 index 000000000..1731e17fb --- /dev/null +++ b/.github/workflows/Dockerfile.build-libs @@ -0,0 +1,25 @@ +# Dockerfile +FROM ubuntu:20.04 + +ENV DEBIAN_FRONTEND=noninteractive + +# Base tools & dependencies for building Go c-shared libs and running protoc +RUN apt-get update && \ + apt-get install -y software-properties-common && \ + add-apt-repository ppa:longsleep/golang-backports && \ + apt-get update && \ + apt-get install -y golang-go protobuf-compiler protobuf-compiler-grpc && \ + rm -rf /var/lib/apt/lists/* + +# Go env +ENV GOPATH=/go +ENV GOBIN=/go/bin +ENV PATH=$GOBIN:/usr/local/go/bin:/usr/lib/go/bin:$PATH + +# Install protoc Go plugins +RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest && \ + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + +WORKDIR /workspace + +CMD ["/bin/bash"] diff --git a/.github/workflows/Dockerfile.centos-php-test b/.github/workflows/Dockerfile.centos-php-test new file mode 100644 index 000000000..25b84eee7 --- /dev/null +++ b/.github/workflows/Dockerfile.centos-php-test @@ -0,0 +1,33 @@ +# syntax=docker/dockerfile:1.7 +# CentOS Stream 9 test image with PHP (from Remi) preinstalled per version, +# plus httpd (mod_php), nginx + php-fpm, MySQL server and Python deps. + +ARG BASE_IMAGE=quay.io/centos/centos:stream9 +ARG PHP_VERSION=8.3 + +FROM ${BASE_IMAGE} +SHELL ["/bin/bash", "-euo", "pipefail", "-c"] + + +# Remi repo + chosen PHP stream +ARG PHP_VERSION +RUN yum install -y yum-utils +RUN dnf -y install https://rpms.remirepo.net/enterprise/remi-release-9.rpm +RUN yum install -y gcc +RUN yum install -y python3-devel +RUN dnf --assumeyes module reset php +RUN dnf --assumeyes --nogpgcheck module install php:remi-${PHP_VERSION} +RUN dnf --assumeyes install php-pdo +RUN dnf --assumeyes install php-mysqlnd +RUN yum install -y mod_php nginx php-fpm procps-ng mysql-server + + +# Python deps used by your test harness +RUN python3 -m pip install --no-cache-dir --upgrade pip \ + && python3 -m pip install --no-cache-dir flask requests psutil + + RUN yum install -y httpd +# Quality-of-life +ENV TZ=Etc/UTC +WORKDIR /work +CMD ["bash"] diff --git a/.github/workflows/Dockerfile.ubuntu-php-test b/.github/workflows/Dockerfile.ubuntu-php-test new file mode 100644 index 000000000..a97f38940 --- /dev/null +++ b/.github/workflows/Dockerfile.ubuntu-php-test @@ -0,0 +1,113 @@ +# syntax=docker/dockerfile:1.7 +FROM ubuntu:24.04 + +ARG DEBIAN_FRONTEND=noninteractive +ARG PHP_VERSION=7.2 + +ENV PHP_VERSION=${PHP_VERSION} + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + ca-certificates curl gnupg lsb-release tzdata locales \ + software-properties-common apt-transport-https \ + git make unzip xz-utils \ + # web servers & DB (installed later after PPA) + && rm -rf /var/lib/apt/lists/* + +# Timezone to UTC +RUN ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime && \ + echo "Etc/UTC" > /etc/timezone && \ + dpkg-reconfigure -f noninteractive tzdata + + +RUN add-apt-repository -y universe && \ + add-apt-repository -y ppa:ondrej/php + +RUN apt-get update + +RUN set -eux; \ + PHP_PKG="php${PHP_VERSION}"; \ + apt-get install -y --no-install-recommends \ + nginx \ + apache2 \ + mariadb-server \ + ${PHP_PKG} ${PHP_PKG}-cli ${PHP_PKG}-common ${PHP_PKG}-fpm \ + ${PHP_PKG}-curl ${PHP_PKG}-sqlite3 ${PHP_PKG}-mysql \ + ${PHP_PKG}-mbstring ${PHP_PKG}-xml ${PHP_PKG}-zip ${PHP_PKG}-opcache \ + libapache2-mod-php${PHP_VERSION} \ + ; \ + # Apache: switch to prefork for mod_php scenario and enable rewrite + a2dismod mpm_event || true; \ + a2dismod mpm_worker || true; \ + a2enmod mpm_prefork rewrite || true + +# ---- Python toolchain used by tests ---- +ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + VIRTUAL_ENV=/opt/ci-venv \ + PATH="/opt/ci-venv/bin:${PATH}" + +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 python3-venv python3-pip python3-dev \ + && python3 -m venv "$VIRTUAL_ENV" \ + && "$VIRTUAL_ENV/bin/pip" install --no-cache-dir \ + flask pandas psutil requests \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# PHP-CGI + Apache CGI modules for tests that require CGI +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + php${PHP_VERSION}-cgi \ + apache2-bin; \ + a2enmod cgi cgid || true; \ + mkdir -p /usr/lib/cgi-bin; \ + # Provide a php-cgi wrapper in the standard location + ln -sf /usr/bin/php-cgi /usr/lib/cgi-bin/php-cgi + +# Helper: start MariaDB +RUN mkdir -p /usr/local/bin /var/lib/mysql /run/mysqld && \ + printf '%s\n' '#!/usr/bin/env bash' \ + 'set -euo pipefail' \ + 'mkdir -p /var/lib/mysql /run/mysqld' \ + 'chown -R mysql:mysql /var/lib/mysql /run/mysqld' \ + 'if [ ! -d /var/lib/mysql/mysql ]; then' \ + ' mysqld --initialize-insecure --user=mysql --datadir=/var/lib/mysql' \ + 'fi' \ + 'mysqld --user=mysql --datadir=/var/lib/mysql &' \ + 'pid=$!' \ + 'for i in {1..30}; do mysqladmin ping --silent && break; sleep 1; done' \ + 'mysql -u root -e "CREATE DATABASE IF NOT EXISTS db;" || true' \ + 'mysql -u root -e "ALTER USER '\''root'\''@'\''localhost'\'' IDENTIFIED BY '\''pwd'\''; FLUSH PRIVILEGES;" || true' \ + 'wait $pid' \ + > /usr/local/bin/start-mariadb && \ + chmod +x /usr/local/bin/start-mariadb + +# Robust Apache PHP switcher (handles module names, MPM, restart, verification) +RUN printf '%s\n' '#!/usr/bin/env bash' \ + 'set -euo pipefail' \ + 'ver="${1:-${PHP_VERSION:-8.2}}"' \ + 'a2dismod mpm_event >/dev/null 2>&1 || true' \ + 'a2dismod mpm_worker >/dev/null 2>&1 || true' \ + 'a2enmod mpm_prefork >/dev/null 2>&1 || true' \ + 'if ! a2query -m "php${ver}" >/dev/null 2>&1; then' \ + ' apt-get update && apt-get install -y --no-install-recommends "libapache2-mod-php${ver}"' \ + 'fi' \ + 'for m in php5 php7 php7.0 php7.1 php7.2 php7.3 php7.4 php8 php8.0 php8.1 php8.2 php8.3 php8.4; do' \ + ' a2query -m "$m" >/dev/null 2>&1 && a2dismod "$m" >/dev/null 2>&1 || true' \ + 'done' \ + 'a2enmod "php${ver}"' \ + 'apache2ctl -t' \ + 'apache2ctl -k graceful || apache2ctl -k restart' \ + 'if ! apache2ctl -M 2>/dev/null | grep -Eiq "php[0-9]*_module"; then' \ + ' echo "Apache does not have a PHP module loaded:"' \ + ' apache2ctl -M || true' \ + ' exit 1' \ + 'fi' \ + 'echo "Apache now using mod_php for PHP ${ver}"' \ + > /usr/local/bin/a2-switch-php && \ + chmod +x /usr/local/bin/a2-switch-php + + +WORKDIR /work + diff --git a/.github/workflows/build-centos-php-test-images.yml b/.github/workflows/build-centos-php-test-images.yml new file mode 100644 index 000000000..181e1fe36 --- /dev/null +++ b/.github/workflows/build-centos-php-test-images.yml @@ -0,0 +1,94 @@ +name: Build CentOS PHP test images + +on: + workflow_dispatch: + push: + paths: + - .github/workflows/Dockerfile.centos-php-test + - .github/workflows/build-centos-php-test-images.yml + +env: + REGISTRY: ghcr.io + IMAGE_NAME: aikidosec/firewall-php-test-centos + VERSION: v1 + +jobs: + build-amd64: + runs-on: ubuntu-24.04 + strategy: + matrix: + php_version: ['7.4','8.0','8.1','8.2','8.3','8.4'] + fail-fast: false + permissions: { contents: read, packages: write } + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build & push (amd64) + uses: docker/build-push-action@v6 + with: + context: . + file: .github/workflows/Dockerfile.centos-php-test + platforms: linux/amd64 + push: true + build-args: | + PHP_VERSION=${{ matrix.php_version }} + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.php_version }}-amd64-${{ env.VERSION }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ matrix.php_version }}-amd64-${{ env.VERSION }} + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ matrix.php_version }}-amd64-${{ env.VERSION }},mode=max + + build-arm64: + runs-on: ubuntu-24.04-arm + strategy: + matrix: + php_version: ['7.4','8.0','8.1','8.2','8.3','8.4'] + fail-fast: false + permissions: { contents: read, packages: write } + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build & push (arm64) + uses: docker/build-push-action@v6 + with: + context: . + file: .github/workflows/Dockerfile.centos-php-test + platforms: linux/arm64 + push: true + build-args: | + PHP_VERSION=${{ matrix.php_version }} + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.php_version }}-arm64-${{ env.VERSION }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ matrix.php_version }}-arm64-${{ env.VERSION }} + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ matrix.php_version }}-arm64-${{ env.VERSION }},mode=max + + publish-manifests: + runs-on: ubuntu-24.04 + needs: [build-amd64, build-arm64] + strategy: + matrix: + php_version: ['7.4','8.0','8.1','8.2','8.3','8.4'] + fail-fast: false + permissions: { contents: read, packages: write } + steps: + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Create multi-arch manifest + run: | + IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + V=${{ matrix.php_version }} + docker buildx imagetools create \ + --tag ${IMAGE}:${V}-${{ env.VERSION }} \ + ${IMAGE}:${V}-amd64-${{ env.VERSION }} \ + ${IMAGE}:${V}-arm64-${{ env.VERSION }} diff --git a/.github/workflows/build-extension-images.yml b/.github/workflows/build-extension-images.yml new file mode 100644 index 000000000..be8def81e --- /dev/null +++ b/.github/workflows/build-extension-images.yml @@ -0,0 +1,113 @@ +name: Create images for building extension + +on: + workflow_dispatch: + push: + paths: + - .github/workflows/Dockerfile.build-extension + - .github/workflows/build-extension-images.yml + +env: + REGISTRY: ghcr.io + IMAGE_NAME: aikidosec/firewall-php-build-extension + VERSION: v1 + +jobs: + build-amd64: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + php_version: ['7.2','7.3','7.4','8.0','8.1','8.2','8.3','8.4'] + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build & push (amd64) + uses: docker/build-push-action@v6 + with: + context: . + file: .github/workflows/Dockerfile.build-extension + target: dev + platforms: linux/amd64 + push: true + build-args: | + PHP_VERSION=${{ matrix.php_version }} + PHP_SRC_REF=PHP-${{ matrix.php_version }} + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.php_version }}-amd64-${{ env.VERSION }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ matrix.php_version }}-amd64-${{ env.VERSION }} + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ matrix.php_version }}-amd64-${{ env.VERSION }},mode=max + + build-arm64: + runs-on: ubuntu-24.04-arm + strategy: + fail-fast: false + matrix: + php_version: ['7.2','7.3','7.4','8.0','8.1','8.2','8.3','8.4'] + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build & push (arm64) + uses: docker/build-push-action@v6 + with: + context: . + file: .github/workflows/Dockerfile.build-extension + target: dev + platforms: linux/arm64 + push: true + build-args: | + PHP_VERSION=${{ matrix.php_version }} + PHP_SRC_REF=PHP-${{ matrix.php_version }} + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.php_version }}-arm64-${{ env.VERSION }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ matrix.php_version }}-arm64-${{ env.VERSION }} + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ matrix.php_version }}-arm64-${{ env.VERSION }},mode=max + + + # ---- stitch images into multi-arch manifests ---- + publish-manifests: + runs-on: ubuntu-24.04 + needs: [build-amd64, build-arm64] + strategy: + fail-fast: false + matrix: + php_version: ['7.2','7.3','7.4','8.0','8.1','8.2','8.3','8.4'] + permissions: + contents: read + packages: write + steps: + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create multi-arch manifest + run: | + IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + V=${{ matrix.php_version }} + docker buildx imagetools create \ + --tag ${IMAGE}:${V}-${{ env.VERSION }} \ + ${IMAGE}:${V}-amd64-${{ env.VERSION }} \ + ${IMAGE}:${V}-arm64-${{ env.VERSION }} \ No newline at end of file diff --git a/.github/workflows/build-libs-image.yml b/.github/workflows/build-libs-image.yml new file mode 100644 index 000000000..9c75be196 --- /dev/null +++ b/.github/workflows/build-libs-image.yml @@ -0,0 +1,87 @@ +name: Build libs toolchain image + +on: + workflow_dispatch: + push: + paths: + - .github/workflows/Dockerfile.build-libs + - .github/workflows/build-libs-image.yml + +env: + REGISTRY: ghcr.io + IMAGE_NAME_LIBS: aikidosec/firewall-php-build-libs + VERSION: v1 + +jobs: + build-amd64: + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build & push (amd64) + uses: docker/build-push-action@v6 + with: + context: . + file: .github/workflows/Dockerfile.build-libs + platforms: linux/amd64 + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LIBS }}:${{ env.VERSION }}-amd64 + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LIBS }}:cache-${{ env.VERSION }}-amd64 + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LIBS }}:cache-${{ env.VERSION }}-amd64,mode=max + + build-arm64: + runs-on: ubuntu-24.04-arm + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build & push (arm64) + uses: docker/build-push-action@v6 + with: + context: . + file: .github/workflows/Dockerfile.build-libs + platforms: linux/arm64 + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LIBS }}:${{ env.VERSION }}-arm64 + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LIBS }}:cache-${{ env.VERSION }}-arm64 + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LIBS }}:cache-${{ env.VERSION }}-arm64,mode=max + + publish-manifest: + runs-on: ubuntu-24.04 + needs: [build-amd64, build-arm64] + permissions: + contents: read + packages: write + steps: + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Create multi-arch manifest + run: | + IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LIBS }} + VERSION=${{ env.VERSION }} + docker buildx imagetools create \ + --tag ${IMAGE}:${VERSION} \ + --tag ${IMAGE}:latest \ + ${IMAGE}:${VERSION}-amd64 \ + ${IMAGE}:${VERSION}-arm64 diff --git a/.github/workflows/build-ubuntu-php-test-images.yml b/.github/workflows/build-ubuntu-php-test-images.yml new file mode 100644 index 000000000..767e389e5 --- /dev/null +++ b/.github/workflows/build-ubuntu-php-test-images.yml @@ -0,0 +1,89 @@ +name: Build Ubuntu PHP test images + +on: + workflow_dispatch: + push: + paths: + - .github/workflows/Dockerfile.ubuntu-php-test + - .github/workflows/build-ubuntu-php-test-images.yml + +env: + REGISTRY: ghcr.io + IMAGE_NAME: aikidosec/firewall-php-test-ubuntu + VERSION: v1 + +jobs: + build-amd64: + runs-on: ubuntu-24.04 + strategy: + matrix: { php_version: ['7.2','7.3','7.4','8.0','8.1','8.2','8.3','8.4'] } + fail-fast: false + permissions: { contents: read, packages: write } + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: docker/build-push-action@v6 + with: + context: . + file: .github/workflows/Dockerfile.ubuntu-php-test + platforms: linux/amd64 + push: true + build-args: | + PHP_VERSION=${{ matrix.php_version }} + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.php_version }}-amd64-${{ env.VERSION }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ matrix.php_version }}-amd64-${{ env.VERSION }} + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ matrix.php_version }}-amd64-${{ env.VERSION }},mode=max + + build-arm64: + runs-on: ubuntu-24.04-arm + strategy: + matrix: { php_version: ['7.2','7.3','7.4','8.0','8.1','8.2','8.3','8.4'] } + fail-fast: false + permissions: { contents: read, packages: write } + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: docker/build-push-action@v6 + with: + context: . + file: .github/workflows/Dockerfile.ubuntu-php-test + platforms: linux/arm64 + push: true + build-args: | + PHP_VERSION=${{ matrix.php_version }} + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.php_version }}-arm64-${{ env.VERSION }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ matrix.php_version }}-arm64-${{ env.VERSION }} + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ matrix.php_version }}-arm64-${{ env.VERSION }},mode=max + + publish-manifests: + runs-on: ubuntu-24.04 + needs: [build-amd64, build-arm64] + strategy: + matrix: { php_version: ['7.2','7.3','7.4','8.0','8.1','8.2','8.3','8.4'] } + fail-fast: false + permissions: { contents: read, packages: write } + steps: + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Create multi-arch manifest + run: | + IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + V=${{ matrix.php_version }} + docker buildx imagetools create \ + --tag ${IMAGE}:${V}-${{ env.VERSION }} \ + ${IMAGE}:${V}-amd64-${{ env.VERSION }} \ + ${IMAGE}:${V}-arm64-${{ env.VERSION }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 996d7f3b7..ec92ee47b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: build_libs: runs-on: ${{ matrix.os }} container: - image: ubuntu:20.04 + image: ghcr.io/aikidosec/firewall-php-build-libs:v1 strategy: matrix: os: [ ubuntu-24.04, ubuntu-24.04-arm ] @@ -26,20 +26,6 @@ jobs: - name: Get Arch run: echo "ARCH=$(uname -m)" >> $GITHUB_ENV - - name: Install dependencies - run: | - apt-get update - apt-get install -y software-properties-common - add-apt-repository ppa:longsleep/golang-backports - apt-get update - apt-get install -y golang-go protobuf-compiler protobuf-compiler-grpc - - - name: GO setup - run: | - go install google.golang.org/protobuf/cmd/protoc-gen-go@latest - go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest - echo "$HOME/go/bin" >> $GITHUB_PATH - - name: Get Aikido version run: | AIKIDO_VERSION=$(grep '#define PHP_AIKIDO_VERSION' lib/php-extension/include/php_aikido.h | awk -F'"' '{print $2}') @@ -56,8 +42,9 @@ jobs: go get google.golang.org/grpc go get github.com/stretchr/testify/assert go test ./... - go build -ldflags "-s -w" -o ../../build/aikido-agent + go build -buildvcs=false -ldflags "-s -w" -o ../../build/aikido-agent ls -l ../../build + - name: Build Aikido Request Processor run: | @@ -92,7 +79,7 @@ jobs: build_php_extension: runs-on: ${{ matrix.os }} - container: ubuntu:20.04 + container: ghcr.io/aikidosec/firewall-php-build-extension:${{ matrix.php_version }}-v1 strategy: matrix: php_version: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] @@ -103,18 +90,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Install dependencies - run: | - DEBIAN_FRONTEND=noninteractive apt-get update - DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata - ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime - echo "Etc/UTC" > /etc/timezone - DEBIAN_FRONTEND=noninteractive dpkg-reconfigure -f noninteractive tzdata - apt-get install -y --no-install-recommends build-essential autoconf bison re2c pkg-config libxml2-dev libsqlite3-dev libcurl4-openssl-dev libssl-dev libzip-dev libonig-dev libjpeg-dev libpng-dev libwebp-dev libicu-dev libreadline-dev libxslt1-dev default-libmysqlclient-dev git wget tar ca-certificates - update-ca-certificates - git config --global http.sslCAInfo /etc/ssl/certs/ca-certificates.crt - git config --global http.sslVerify true - - name: Get Arch run: echo "ARCH=$(uname -m)" >> $GITHUB_ENV @@ -125,48 +100,12 @@ jobs: echo "AIKIDO_VERSION=$AIKIDO_VERSION" >> $GITHUB_ENV echo "AIKIDO_ARTIFACT=aikido-extension-php-${{ matrix.php_version }}" >> $GITHUB_ENV - - name: Checkout php-src - uses: actions/checkout@v4 - with: - repository: php/php-src - ref: PHP-${{ matrix.php_version }} - path: php-src - fetch-depth: 1 - - - name: Clone php-src - run: | - cd php-src - ./buildconf --force - - - name: Configure & build PHP - run: | - cd php-src - ./configure \ - --prefix=/usr/local \ - --with-config-file-path=/usr/local/lib \ - --with-config-file-scan-dir=/usr/local/etc/php/conf.d \ - --enable-mbstring \ - --enable-pcntl \ - --enable-intl \ - --with-curl \ - --with-mysqli \ - --with-openssl \ - --with-zlib \ - --with-zip - make -j"$(nproc)" - make install - - - name: Verify PHP build - run: | - php -v - php -m | grep -E 'curl|mysqli' || (echo "Required extensions missing" && php -m && exit 1) - - - name: Check PHP setup run: | which php php -v php -i + php -m | grep -E 'curl|mysqli' || (echo "Required extensions missing" && php -m && exit 1) - name: Build extension run: | @@ -176,7 +115,7 @@ jobs: phpize cd ../../build CXX=g++ CXXFLAGS="-fPIC -O2 -I../lib/php-extension/include" LDFLAGS="-lstdc++" ../lib/php-extension/configure - make + make -j"$(nproc)" - name: Version Aikido extension run: | @@ -357,7 +296,7 @@ jobs: test_php_centos: runs-on: ${{ matrix.os }} container: - image: quay.io/centos/centos:stream9 + image: ghcr.io/aikidosec/firewall-php-test-centos:${{ matrix.php_version }}-v1 options: --privileged needs: [ build_rpm ] strategy: @@ -366,6 +305,7 @@ jobs: server: ['nginx-php-fpm', 'apache-mod-php', 'php-built-in'] os: ['ubuntu-24.04', 'ubuntu-24.04-arm'] fail-fast: false + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -374,26 +314,14 @@ jobs: run: | uname -a cat /etc/centos-release - yum install -y yum-utils - dnf install -y https://rpms.remirepo.net/enterprise/remi-release-9.rpm - yum install -y gcc - yum install -y python3-devel - pip3 install flask - pip3 install requests - pip3 install psutil - yum install -y httpd - dnf --assumeyes module reset php - dnf --assumeyes --nogpgcheck module install php:remi-${{ matrix.php_version }} - dnf --assumeyes install php-pdo - dnf --assumeyes install php-mysqlnd - yum install -y mod_php - yum install -y nginx - yum install -y php-fpm - dnf install -y procps-ng + php -v + httpd -v || true + nginx -v || true + which php-fpm && php-fpm -v || true + rpm -qa | grep -E '^php(-|$)' | sort - name: Install and start MySQL run: | - yum install -y mysql-server mkdir -p /var/lib/mysql mysqld --initialize-insecure --datadir=/var/lib/mysql mysqld -u root --datadir=/var/lib/mysql --socket=/var/lib/mysql/mysql.sock & @@ -453,12 +381,11 @@ jobs: test_php_ubuntu: runs-on: ${{ matrix.os }} container: - image: ${{ matrix.container }} + image: ghcr.io/aikidosec/firewall-php-test-ubuntu:${{ matrix.php_version }}-v1 options: --privileged needs: [ build_deb ] strategy: matrix: - container: ['ubuntu:24.04'] os: ['ubuntu-24.04', 'ubuntu-24.04-arm'] php_version: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] server: ['nginx-php-fpm', 'apache-mod-php', 'php-built-in'] @@ -483,78 +410,19 @@ jobs: pattern: | ${{ env.AIKIDO_DEB }} - - name: Setup nginx & php-fpm + # MariaDB startup compatible with your current approach + - name: Start MariaDB (background) run: | - DEBIAN_FRONTEND=noninteractive apt-get update - DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata - ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime - echo "Etc/UTC" > /etc/timezone - DEBIAN_FRONTEND=noninteractive dpkg-reconfigure -f noninteractive tzdata - apt-get install -y nginx php-fpm + start-mariadb & # provided by the image + sleep 5 + mysql -u root -ppwd -e "SELECT 1" || (echo "MySQL not up" && exit 1) - - name: Setup Apache (mod_php) + # For Apache mod_php tests, ensure the right PHP module is active + - name: Ensure Apache uses PHP ${{ matrix.php_version }} + if: matrix.server == 'apache-mod-php' run: | - apt-get install -y apache2 - a2dismod mpm_event - a2dismod mpm_worker - a2enmod mpm_prefork - a2enmod rewrite - - - name: Install MariaDB server - run: | - apt-get install -y mariadb-server - mkdir -p /var/lib/mysql - mkdir -p /run/mysqld - mysqld --user=root --datadir=/var/lib/mysql & - sleep 10 - mysql -u root -e "CREATE DATABASE IF NOT EXISTS db;" - mysql -u root -e "SET PASSWORD FOR 'root'@'localhost' = PASSWORD('pwd'); FLUSH PRIVILEGES;" - - - name: Setup PHP - uses: shivammathur/setup-php@27853eb8b46dc01c33bf9fef67d98df2683c3be2 - with: - php-version: ${{ matrix.php_version }} - extensions: curl, sqlite3, mysqli - coverage: none - - - name: Test MySQL connection with mysqli - run: | - php -r ' - $mysqli = new mysqli("localhost", "root", "pwd", "db"); - if ($mysqli->connect_error) { - echo "MySQL connection failed: " . $mysqli->connect_error . "\n"; - exit(1); - } else { - echo "MySQL connection successful\n"; - $mysqli->close(); - } - ' - - - name: Check PHP setup - run: | - php_versions="php7.3 php7.4 php8.0 php8.1 php8.2 php8.3" - for version in $php_versions; do - if a2query -m "$version" > /dev/null 2>&1; then - echo "Disabling $version..." - a2dismod "$version" - else - echo "$version is not installed." - fi - done - DEBIAN_FRONTEND=noninteractive apt-get update - DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata - ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime - echo "Etc/UTC" > /etc/timezone - DEBIAN_FRONTEND=noninteractive dpkg-reconfigure -f noninteractive tzdata - apt-get install -y libapache2-mod-php${{ matrix.php_version }} - a2enmod php${{ matrix.php_version }} - apt-get install -y php${{ matrix.php_version }}-mysqli - apt-get install -y php${{ matrix.php_version }}-pdo - php -i - - - name: Setup Python - run: | - apt-get install -y python3 python3-flask python3-pandas python3-psutil python3-requests + a2-switch-php ${{ matrix.php_version }} + - name: Install DEB run: | @@ -578,3 +446,39 @@ jobs: if-no-files-found: ignore path: | ${{ github.workspace }}/tests/cli/**/*.diff + + test_php_qa_action: + runs-on: ubuntu-latest + needs: [ build_deb ] + steps: + + - name: Checkout zen-demo-php + uses: actions/checkout@v4 + with: + repository: Aikido-demo-apps/zen-demo-php + path: zen-demo-php + ref: dev-testing + submodules: recursive + + - name: Get Arch + run: echo "ARCH=$(uname -m)" >> $GITHUB_ENV + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: aikido-php-firewall.${{ env.ARCH }}.deb + path: ./zen-demo-php + + - name: Overwrite aikido.sh install script + run: | + echo "dpkg -i -E \"/var/www/html/aikido-php-firewall.\$(uname -i).deb\"" > ./zen-demo-php/.fly/scripts/aikido.sh + + - name: Run Firewall QA Tests + uses: AikidoSec/firewall-tester-action@releases/v1 + with: + dockerfile_path: ./zen-demo-php/Dockerfile + extra_args: '--env-file=./zen-demo-php/.env.example -e APP_KEY=base64:W2v6u6VR4lURkxuMT9xZ6pdhXSt5rxsmWTbd1HGqlIM=' + sleep_before_test: 20 + skip_tests: test_wave_attack,test_rate_limiting_group_id_1_minute + max_parallel_tests: 7 + ignore_failures: true \ No newline at end of file diff --git a/README.md b/README.md index 7fcaf3cf5..d8084457d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![Zen by Aikido for PHP](./docs/banner.svg) -# Zen, in-app firewall for PHP | by Aikido +# Zen, in-app firewall for PHP | by Aikido Zen, your in-app firewall for peace of mind– at runtime. @@ -38,25 +38,25 @@ Prerequisites: ##### x86_64 ``` -rpm -Uvh --oldpackage https://github.com/AikidoSec/firewall-php/releases/download/v1.3.4/aikido-php-firewall.x86_64.rpm +rpm -Uvh --oldpackage https://github.com/AikidoSec/firewall-php/releases/download/v1.3.5/aikido-php-firewall.x86_64.rpm ``` ##### arm64 / aarch64 ``` -rpm -Uvh --oldpackage https://github.com/AikidoSec/firewall-php/releases/download/v1.3.4/aikido-php-firewall.aarch64.rpm +rpm -Uvh --oldpackage https://github.com/AikidoSec/firewall-php/releases/download/v1.3.5/aikido-php-firewall.aarch64.rpm ``` #### For Debian-based Systems (Debian, Ubuntu) ##### x86_64 ``` -curl -L -O https://github.com/AikidoSec/firewall-php/releases/download/v1.3.4/aikido-php-firewall.x86_64.deb +curl -L -O https://github.com/AikidoSec/firewall-php/releases/download/v1.3.5/aikido-php-firewall.x86_64.deb dpkg -i -E ./aikido-php-firewall.x86_64.deb ``` ##### arm64 / aarch64 ``` -curl -L -O https://github.com/AikidoSec/firewall-php/releases/download/v1.3.4/aikido-php-firewall.aarch64.deb +curl -L -O https://github.com/AikidoSec/firewall-php/releases/download/v1.3.5/aikido-php-firewall.aarch64.deb dpkg -i -E ./aikido-php-firewall.aarch64.deb ``` diff --git a/docs/aws-elastic-beanstalk.md b/docs/aws-elastic-beanstalk.md index a49c79f0e..de6009eef 100644 --- a/docs/aws-elastic-beanstalk.md +++ b/docs/aws-elastic-beanstalk.md @@ -4,7 +4,7 @@ ``` commands: aikido-php-firewall: - command: "rpm -Uvh --oldpackage https://github.com/AikidoSec/firewall-php/releases/download/v1.3.4/aikido-php-firewall.x86_64.rpm" + command: "rpm -Uvh --oldpackage https://github.com/AikidoSec/firewall-php/releases/download/v1.3.5/aikido-php-firewall.x86_64.rpm" ignoreErrors: true files: diff --git a/docs/fly-io.md b/docs/fly-io.md index 3ae90f458..bd45b3315 100644 --- a/docs/fly-io.md +++ b/docs/fly-io.md @@ -32,7 +32,7 @@ Create a script to install the Aikido PHP Firewall during deployment: #!/usr/bin/env bash cd /tmp -curl -L -O https://github.com/AikidoSec/firewall-php/releases/download/v1.3.4/aikido-php-firewall.x86_64.deb +curl -L -O https://github.com/AikidoSec/firewall-php/releases/download/v1.3.5/aikido-php-firewall.x86_64.deb dpkg -i -E ./aikido-php-firewall.x86_64.deb ``` diff --git a/docs/laravel-forge.md b/docs/laravel-forge.md index ec3673250..d2cfefdb2 100644 --- a/docs/laravel-forge.md +++ b/docs/laravel-forge.md @@ -21,7 +21,7 @@ cd /tmp # Install commands from the "Manual install" section below, based on your OS -curl -L -O https://github.com/AikidoSec/firewall-php/releases/download/v1.3.4/aikido-php-firewall.x86_64.deb +curl -L -O https://github.com/AikidoSec/firewall-php/releases/download/v1.3.5/aikido-php-firewall.x86_64.deb dpkg -i -E ./aikido-php-firewall.x86_64.deb # Restarting the php services in order to load the Aikido PHP Firewall diff --git a/lib/API.h b/lib/API.h index 5e72b72e1..e9881dee4 100644 --- a/lib/API.h +++ b/lib/API.h @@ -46,6 +46,8 @@ enum CALLBACK_ID { // URL were the request was actually made) OUTGOING_REQUEST_PORT, OUTGOING_REQUEST_RESOLVED_IP, + OUTGOING_REQUEST_RESPONSE_CODE, + OUTGOING_REQUEST_REDIRECT_URL, CMD, diff --git a/lib/agent/aikido_types/init_data.go b/lib/agent/aikido_types/init_data.go index afe70e248..7f5901f1e 100644 --- a/lib/agent/aikido_types/init_data.go +++ b/lib/agent/aikido_types/init_data.go @@ -16,13 +16,13 @@ type EnvironmentConfigData struct { PlatformVersion string `json:"platform_version"` // PHP version Endpoint string `json:"endpoint,omitempty"` // default: 'https://guard.aikido.dev/' ConfigEndpoint string `json:"config_endpoint,omitempty"` // default: 'https://runtime.aikido.dev/' - DiskLogs bool `json:"disk_logs,omitempty"` // default: false } type AikidoConfigData struct { ConfigMutex sync.Mutex Token string `json:"token,omitempty"` // default: '' LogLevel string `json:"log_level,omitempty"` // default: 'INFO' + DiskLogs bool `json:"disk_logs,omitempty"` // default: false Blocking bool `json:"blocking,omitempty"` // default: false LocalhostAllowedByDefault bool `json:"localhost_allowed_by_default,omitempty"` // default: true CollectApiSchema bool `json:"collect_api_schema,omitempty"` // default: true diff --git a/lib/agent/globals/constants.go b/lib/agent/globals/constants.go index 5bb949395..38e5239eb 100644 --- a/lib/agent/globals/constants.go +++ b/lib/agent/globals/constants.go @@ -1,7 +1,7 @@ package globals const ( - Version = "1.3.4" + Version = "1.3.5" ConfigUpdatedAtMethod = "GET" ConfigUpdatedAtAPI = "/config" ConfigAPIMethod = "GET" diff --git a/lib/agent/go.mod b/lib/agent/go.mod index bc6b67d45..aa5de276e 100644 --- a/lib/agent/go.mod +++ b/lib/agent/go.mod @@ -6,16 +6,16 @@ toolchain go1.23.3 require ( github.com/stretchr/testify v1.9.0 - google.golang.org/grpc v1.74.2 + google.golang.org/grpc v1.75.1 google.golang.org/protobuf v1.36.6 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/net v0.40.0 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.25.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect + golang.org/x/text v0.26.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/lib/agent/go.sum b/lib/agent/go.sum index 8c68dd312..9ef849ffd 100644 --- a/lib/agent/go.sum +++ b/lib/agent/go.sum @@ -24,6 +24,8 @@ golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= @@ -40,6 +42,8 @@ golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= @@ -48,6 +52,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= @@ -58,6 +64,8 @@ google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= diff --git a/lib/agent/grpc/config.go b/lib/agent/grpc/config.go index f460abb84..3e8236c66 100644 --- a/lib/agent/grpc/config.go +++ b/lib/agent/grpc/config.go @@ -5,16 +5,20 @@ import ( "main/log" ) -func storeConfig(token, logLevel string, blocking, localhostAllowedByDefault, collectApiSchema bool) { +func storeConfig(token, logLevel string, diskLogs, blocking, localhostAllowedByDefault, collectApiSchema bool) { globals.AikidoConfig.ConfigMutex.Lock() defer globals.AikidoConfig.ConfigMutex.Unlock() - globals.AikidoConfig.Token = token + if token != "" { + globals.AikidoConfig.Token = token + } globals.AikidoConfig.LogLevel = logLevel + globals.AikidoConfig.DiskLogs = diskLogs globals.AikidoConfig.Blocking = blocking globals.AikidoConfig.LocalhostAllowedByDefault = localhostAllowedByDefault globals.AikidoConfig.CollectApiSchema = collectApiSchema log.SetLogLevel(globals.AikidoConfig.LogLevel) + log.Init() log.Debugf("Updated Aikido Config with the one passed via gRPC!") } diff --git a/lib/agent/grpc/server.go b/lib/agent/grpc/server.go index 3ed556fe7..368337418 100644 --- a/lib/agent/grpc/server.go +++ b/lib/agent/grpc/server.go @@ -28,7 +28,7 @@ func (s *server) OnConfig(ctx context.Context, req *protos.Config) (*emptypb.Emp if previousToken == "" && req.GetToken() != "" { // Update the config only if the token was not previously set and the new token that we get from gRPC is not empty - storeConfig(req.GetToken(), req.GetLogLevel(), req.GetBlocking(), req.GetLocalhostAllowedByDefault(), req.GetCollectApiSchema()) + storeConfig(req.GetToken(), req.GetLogLevel(), req.GetDiskLogs(), req.GetBlocking(), req.GetLocalhostAllowedByDefault(), req.GetCollectApiSchema()) // First time the token is set -> we can start reporting things to cloud cloud.SendStartEvent() diff --git a/lib/agent/log/log.go b/lib/agent/log/log.go index bd0fed803..771b43108 100644 --- a/lib/agent/log/log.go +++ b/lib/agent/log/log.go @@ -113,7 +113,10 @@ func SetLogLevel(level string) error { } func Init() { - if !globals.EnvironmentConfig.DiskLogs { + if !globals.AikidoConfig.DiskLogs { + return + } + if logFile != nil { return } currentTime := time.Now() @@ -129,7 +132,7 @@ func Init() { } func Uninit() { - if !globals.EnvironmentConfig.DiskLogs { + if !globals.AikidoConfig.DiskLogs { return } logger.SetOutput(os.Stdout) diff --git a/lib/ipc.proto b/lib/ipc.proto index 9139d83a8..8c275793d 100644 --- a/lib/ipc.proto +++ b/lib/ipc.proto @@ -24,9 +24,10 @@ service Aikido { message Config { string token = 1; string log_level = 2; - bool blocking = 3; - bool localhost_allowed_by_default = 4; - bool collect_api_schema = 5; + bool disk_logs = 3; + bool blocking = 4; + bool localhost_allowed_by_default = 5; + bool collect_api_schema = 6; } message Packages { diff --git a/lib/php-extension/Cache.cpp b/lib/php-extension/Cache.cpp index 323d233bf..ef74d2342 100644 --- a/lib/php-extension/Cache.cpp +++ b/lib/php-extension/Cache.cpp @@ -7,6 +7,10 @@ void RequestCache::Reset() { *this = RequestCache(); } +void EventCache::Copy(EventCache& other) { + *this = other; +} + void EventCache::Reset() { *this = EventCache(); } diff --git a/lib/php-extension/GoWrappers.cpp b/lib/php-extension/GoWrappers.cpp index 0586ab595..f50811e97 100644 --- a/lib/php-extension/GoWrappers.cpp +++ b/lib/php-extension/GoWrappers.cpp @@ -22,7 +22,7 @@ char* GoContextCallback(int callbackId) { break; case CONTEXT_METHOD: ctx = "METHOD"; - ret = server.GetVar("REQUEST_METHOD"); + ret = server.GetMethod(); break; case CONTEXT_ROUTE: ctx = "ROUTE"; @@ -96,6 +96,14 @@ char* GoContextCallback(int callbackId) { ctx = "OUTGOING_REQUEST_RESOLVED_IP"; ret = eventCache.outgoingRequestResolvedIp; break; + case OUTGOING_REQUEST_RESPONSE_CODE: + ctx = "OUTGOING_REQUEST_RESPONSE_CODE"; + ret = eventCache.outgoingRequestResponseCode; + break; + case OUTGOING_REQUEST_REDIRECT_URL: + ctx = "OUTGOING_REQUEST_REDIRECT_URL"; + ret = eventCache.outgoingRequestRedirectUrl; + break; case CMD: ctx = "CMD"; ret = eventCache.cmd; diff --git a/lib/php-extension/HandleFileCompilation.cpp b/lib/php-extension/HandleFileCompilation.cpp index c8faad1a3..ef95be4a8 100644 --- a/lib/php-extension/HandleFileCompilation.cpp +++ b/lib/php-extension/HandleFileCompilation.cpp @@ -1,6 +1,8 @@ #include "Includes.h" zend_op_array* handle_file_compilation(zend_file_handle* file_handle, int type) { + EventCache old_eventCache; + old_eventCache.Copy(eventCache); eventCache.Reset(); switch (type) { case ZEND_INCLUDE: @@ -40,6 +42,6 @@ zend_op_array* handle_file_compilation(zend_file_handle* file_handle, int type) eventId = NO_EVENT_ID; helper_handle_post_file_path_access(eventId); aikido_process_event(eventId, eventCache.functionName); - + eventCache = old_eventCache; return op_array; } diff --git a/lib/php-extension/HandleUrls.cpp b/lib/php-extension/HandleUrls.cpp index c9e6e41b9..8abe6d981 100644 --- a/lib/php-extension/HandleUrls.cpp +++ b/lib/php-extension/HandleUrls.cpp @@ -39,4 +39,6 @@ AIKIDO_HANDLER_FUNCTION(handle_post_curl_exec) { eventCache.outgoingRequestEffectiveUrl = CallPhpFunctionCurlGetInfo(curlHandle, CURLINFO_EFFECTIVE_URL); eventCache.outgoingRequestPort = CallPhpFunctionCurlGetInfo(curlHandle, CURLINFO_PRIMARY_PORT); eventCache.outgoingRequestResolvedIp = CallPhpFunctionCurlGetInfo(curlHandle, CURLINFO_PRIMARY_IP); + eventCache.outgoingRequestResponseCode = CallPhpFunctionCurlGetInfo(curlHandle, CURLINFO_RESPONSE_CODE); + eventCache.outgoingRequestRedirectUrl = CallPhpFunctionCurlGetInfo(curlHandle, CURLINFO_REDIRECT_URL); } diff --git a/lib/php-extension/Log.cpp b/lib/php-extension/Log.cpp index 3e96e33da..be4c04209 100644 --- a/lib/php-extension/Log.cpp +++ b/lib/php-extension/Log.cpp @@ -5,6 +5,10 @@ void Log::Init() { return; } + if (this->logFile) { + return; + } + this->logFilePath = "/var/log/aikido-" + std::string(PHP_AIKIDO_VERSION) + "/aikido-extension-php-" + GetDateTime() + "-" + std::to_string(getpid()) + ".log"; this->logFile = fopen(this->logFilePath.c_str(), "w"); AIKIDO_LOG_INFO("Opened log file %s!\n", this->logFilePath.c_str()); diff --git a/lib/php-extension/RequestProcessor.cpp b/lib/php-extension/RequestProcessor.cpp index 7c9f5da81..aee0129c1 100644 --- a/lib/php-extension/RequestProcessor.cpp +++ b/lib/php-extension/RequestProcessor.cpp @@ -139,6 +139,8 @@ bool RequestProcessor::Init() { return false; } + AIKIDO_GLOBAL(logger).Init(); + AIKIDO_LOG_INFO("Aikido Request Processor initialized successfully!\n"); return true; } diff --git a/lib/php-extension/Server.cpp b/lib/php-extension/Server.cpp index b58462995..100919e81 100644 --- a/lib/php-extension/Server.cpp +++ b/lib/php-extension/Server.cpp @@ -35,6 +35,53 @@ std::string Server::GetVar(const char* var) { return Z_STRVAL_P(data); } +// Return the method from the query param _method (_GET["_method"]) +std::string Server::GetMethodFromQuery() { + zval *get_array; + get_array = zend_hash_str_find(&EG(symbol_table), "_GET", sizeof("_GET") - 1); + if (!get_array || Z_TYPE_P(get_array) != IS_ARRAY) { + return ""; + } + + zval* query_method = zend_hash_str_find(Z_ARRVAL_P(get_array), "_method", sizeof("_method") - 1); + if (!query_method) { + return ""; + } + if (Z_TYPE_P(query_method) != IS_STRING) { + return ""; + } + std::string query_method_str = Z_STRVAL_P(query_method); + return ToUppercase(query_method_str); +} + +// For frameworks like Symfony, Laravel, method override is supported using X-HTTP-METHOD-OVERRIDE or _method query param +// https://github.com/symfony/symfony/blob/b8eaa4be31f2159918e79e5694bc9ff241e0d692/src/Symfony/Component/HttpFoundation/Request.php#L1169-L1215 +std::string Server::GetMethod() { + std::string method = ToUppercase(this->GetVar("REQUEST_METHOD")); + + // TODO: Add a check here to see if the request is from Symfony or Laravel + + if (method != "POST") { + return method; + } + + // X-HTTP-METHOD-OVERRIDE + std::string x_http_method_override = ToUppercase(this->GetVar("HTTP_X_HTTP_METHOD_OVERRIDE")); + if (x_http_method_override != "") { + method = x_http_method_override; + } + + // in case of X-HTTP-METHOD-OVERRIDE is not set, we check the query param _method + if (x_http_method_override == "") { + std::string query_method = this->GetMethodFromQuery(); + if (query_method != "") { + method = query_method; + } + } + + return method; +} + std::string Server::GetRoute() { std::string route = this->GetVar("REQUEST_URI"); size_t pos = route.find("?"); diff --git a/lib/php-extension/Utils.cpp b/lib/php-extension/Utils.cpp index e85d65c28..e39bc6db1 100644 --- a/lib/php-extension/Utils.cpp +++ b/lib/php-extension/Utils.cpp @@ -6,6 +6,12 @@ std::string ToLowercase(const std::string& str) { return result; } +std::string ToUppercase(const std::string& str) { + std::string result = str; + std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { return std::toupper(c); }); + return result; +} + std::string GetRandomNumber() { std::random_device rd; std::mt19937 gen(rd()); diff --git a/lib/php-extension/include/Cache.h b/lib/php-extension/include/Cache.h index 9099af8e4..35ac190df 100644 --- a/lib/php-extension/include/Cache.h +++ b/lib/php-extension/include/Cache.h @@ -24,12 +24,15 @@ class EventCache { std::string outgoingRequestEffectiveUrl; std::string outgoingRequestPort; std::string outgoingRequestResolvedIp; + std::string outgoingRequestResponseCode; + std::string outgoingRequestRedirectUrl; std::string sqlQuery; std::string sqlDialect; EventCache() = default; void Reset(); + void Copy(EventCache& other); }; extern RequestCache requestCache; diff --git a/lib/php-extension/include/Server.h b/lib/php-extension/include/Server.h index 0b1a1070d..48487e0a8 100644 --- a/lib/php-extension/include/Server.h +++ b/lib/php-extension/include/Server.h @@ -9,6 +9,10 @@ class Server { std::string GetVar(const char* var); + std::string GetMethod(); + + std::string GetMethodFromQuery(); + std::string GetRoute(); std::string GetStatusCode(); diff --git a/lib/php-extension/include/Utils.h b/lib/php-extension/include/Utils.h index eef2b1d3e..6313c102b 100644 --- a/lib/php-extension/include/Utils.h +++ b/lib/php-extension/include/Utils.h @@ -4,6 +4,8 @@ std::string ToLowercase(const std::string& str); +std::string ToUppercase(const std::string& str); + std::string GetRandomNumber(); std::string GetTime(); diff --git a/lib/php-extension/include/php_aikido.h b/lib/php-extension/include/php_aikido.h index 63cd7a669..b358dd9ba 100644 --- a/lib/php-extension/include/php_aikido.h +++ b/lib/php-extension/include/php_aikido.h @@ -3,7 +3,7 @@ extern zend_module_entry aikido_module_entry; #define phpext_aikido_ptr &aikido_module_entry -#define PHP_AIKIDO_VERSION "1.3.4" +#define PHP_AIKIDO_VERSION "1.3.5" #if defined(ZTS) && defined(COMPILE_DL_AIKIDO) ZEND_TSRMLS_CACHE_EXTERN() diff --git a/lib/request-processor/context/event_getters.go b/lib/request-processor/context/event_getters.go index 513479fdf..c7d29e947 100644 --- a/lib/request-processor/context/event_getters.go +++ b/lib/request-processor/context/event_getters.go @@ -19,6 +19,15 @@ func GetOutgoingRequestResolvedIp() string { return Context.Callback(C.OUTGOING_REQUEST_RESOLVED_IP) } +func GetOutgoingRequestResponseCode() string { + return Context.Callback(C.OUTGOING_REQUEST_RESPONSE_CODE) +} + +func GetOutgoingRequestRedirectUrl() string { + host, _ := getHostNameAndPort(C.OUTGOING_REQUEST_REDIRECT_URL) + return host +} + func GetFunctionName() string { return Context.Callback(C.FUNCTION_NAME) } @@ -49,6 +58,9 @@ func GetModule() string { func getHostNameAndPort(urlCallbackId int) (string, uint32) { // urlcallbackid is the type of data we request, eg C.OUTGOING_REQUEST_URL urlStr := Context.Callback(urlCallbackId) + // remove all control characters (< 32) and 0x7f(DEL) also replace \@ with @ and remove all whitespace + // url.Parse fails if the url contains control characters + urlStr = helpers.NormalizeRawUrl(urlStr) urlParsed, err := url.Parse(urlStr) if err != nil { return "", 0 diff --git a/lib/request-processor/globals/globals.go b/lib/request-processor/globals/globals.go index e3d79623d..45b1c2c44 100644 --- a/lib/request-processor/globals/globals.go +++ b/lib/request-processor/globals/globals.go @@ -14,5 +14,5 @@ var CloudConfigMutex sync.Mutex var MiddlewareInstalled bool const ( - Version = "1.3.4" + Version = "1.3.5" ) diff --git a/lib/request-processor/go.mod b/lib/request-processor/go.mod index 3b08655ed..cf7a20cb3 100644 --- a/lib/request-processor/go.mod +++ b/lib/request-processor/go.mod @@ -5,18 +5,18 @@ go 1.23.0 toolchain go1.23.3 require ( - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - google.golang.org/grpc v1.74.2 + google.golang.org/grpc v1.75.1 google.golang.org/protobuf v1.36.6 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/net v0.40.0 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.25.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect + golang.org/x/text v0.26.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/lib/request-processor/go.sum b/lib/request-processor/go.sum index 572759c51..a754bcf01 100644 --- a/lib/request-processor/go.sum +++ b/lib/request-processor/go.sum @@ -14,6 +14,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= @@ -24,6 +26,8 @@ golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= @@ -40,6 +44,8 @@ golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= @@ -48,6 +54,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= @@ -58,6 +66,8 @@ google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= diff --git a/lib/request-processor/grpc/client.go b/lib/request-processor/grpc/client.go index 88b1da63d..19c748111 100644 --- a/lib/request-processor/grpc/client.go +++ b/lib/request-processor/grpc/client.go @@ -53,7 +53,7 @@ func SendAikidoConfig() { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() - _, err := client.OnConfig(ctx, &protos.Config{Token: globals.AikidoConfig.Token, LogLevel: globals.AikidoConfig.LogLevel, + _, err := client.OnConfig(ctx, &protos.Config{Token: globals.AikidoConfig.Token, LogLevel: globals.AikidoConfig.LogLevel, DiskLogs: globals.AikidoConfig.DiskLogs, Blocking: globals.AikidoConfig.Blocking, LocalhostAllowedByDefault: globals.AikidoConfig.LocalhostAllowedByDefault, CollectApiSchema: globals.AikidoConfig.CollectApiSchema}) if err != nil { diff --git a/lib/request-processor/handle_sql_queries.go b/lib/request-processor/handle_sql_queries.go index 1ea3bd007..a92f3bddf 100644 --- a/lib/request-processor/handle_sql_queries.go +++ b/lib/request-processor/handle_sql_queries.go @@ -14,7 +14,6 @@ func OnPreSqlQueryExecuted() string { if query == "" || dialect == "" { return "" } - log.Info("Got SQL query: \"", query, "\", dialect: ", dialect) if context.IsEndpointProtectionTurnedOff() { log.Infof("Protection is turned off -> will not run detection logic!") diff --git a/lib/request-processor/handle_urls.go b/lib/request-processor/handle_urls.go index 4c77b556b..03b324769 100644 --- a/lib/request-processor/handle_urls.go +++ b/lib/request-processor/handle_urls.go @@ -86,6 +86,15 @@ func OnPostOutgoingRequest() string { } } + // if the response is 302, we need to check CURLINFO_REDIRECT_URL + responseCode := context.GetOutgoingRequestResponseCode() + if res == nil && responseCode == "302" { + redirectUrl := context.GetOutgoingRequestRedirectUrl() + if redirectUrl != "" { + res = ssrf.CheckEffectiveHostnameForSSRF(redirectUrl) + } + } + if res != nil { /* Throw exception to PHP layer if blocking is enabled -> Response content is not returned to the PHP code */ return attack.ReportAttackDetected(res) diff --git a/lib/request-processor/helpers/normalizeRequestUrl.go b/lib/request-processor/helpers/normalizeRequestUrl.go new file mode 100644 index 000000000..c555433ed --- /dev/null +++ b/lib/request-processor/helpers/normalizeRequestUrl.go @@ -0,0 +1,61 @@ +package helpers + +import ( + "net/url" + "strings" +) + +// remove all control characters (< 32) and 0x7f(DEL) + whitespace +func removeCTLByte(urlStr string) string { + for i := 0; i < len(urlStr); i++ { + if urlStr[i] <= ' ' || urlStr[i] == 0x7f { + urlStr = urlStr[:i] + urlStr[i+1:] + } + } + return urlStr +} + +func removeUserInfo(raw string) string { + schemeEnd := strings.Index(raw, "://") + if schemeEnd == -1 { + // No scheme, can't safely identify authority + return raw + } + + scheme := raw[:schemeEnd+3] + rest := raw[schemeEnd+3:] + + // Authority is up to first '/', '?', or '#' (https://datatracker.ietf.org/doc/html/rfc3986#section-3.2) + authorityEnd := len(rest) + for _, sep := range []string{"/", "?", "#"} { + if idx := strings.Index(rest, sep); idx != -1 && idx < authorityEnd { + authorityEnd = idx + } + } + + authority := rest[:authorityEnd] + path := rest[authorityEnd:] + + // Remove userinfo if present + if at := strings.LastIndex(authority, "@"); at != -1 { + authority = authority[at+1:] + } + + return scheme + authority + path +} + +func UnescapeUrl(urlStr string) string { + unescapedUrl, err := url.QueryUnescape(urlStr) + if err != nil { + return urlStr + } + return unescapedUrl +} + +func NormalizeRawUrl(urlStr string) string { + urlStr = UnescapeUrl(urlStr) + urlStr = removeCTLByte(urlStr) + urlStr = FixURL(urlStr) + urlStr = removeUserInfo(urlStr) + return urlStr +} diff --git a/lib/request-processor/helpers/normalizeRequestUrl_test.go b/lib/request-processor/helpers/normalizeRequestUrl_test.go new file mode 100644 index 000000000..9684d5d9f --- /dev/null +++ b/lib/request-processor/helpers/normalizeRequestUrl_test.go @@ -0,0 +1,23 @@ +package helpers + +import "testing" + +func TestNormalizeRequestUrl(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"http://localhost:4000", "http://localhost:4000"}, + {"http://localhost:4000 ", "http://localhost:4000"}, + {"http://localhost:4000" + "\x00", "http://localhost:4000"}, + {"http://\\@localhost:4000", "http://localhost:4000"}, + {"http://127.1.1.1:4000\\\\\\@127.0.0.1:80/", "http://127.0.0.1:80/"}, + {"https:/localhost:4000", "https://localhost:4000"}, + } + for _, test := range tests { + result := NormalizeRawUrl(test.input) + if result != test.expected { + t.Errorf("For input '%s', expected %v but got %v", test.input, test.expected, result) + } + } +} diff --git a/lib/request-processor/helpers/trimInvisible.go b/lib/request-processor/helpers/trimInvisible.go new file mode 100644 index 000000000..c4ca60be8 --- /dev/null +++ b/lib/request-processor/helpers/trimInvisible.go @@ -0,0 +1,41 @@ +package helpers + +import ( + "strings" + "unicode" +) + +// TrimInvisible trims leading/trailing whitespace, control, and specific +// invisible/format runes similar to PHP Illuminate\Support\Str::trim. +func TrimInvisible(s string) string { + return strings.TrimFunc(s, isTrimRune) +} + +func isTrimRune(r rune) bool { + // 1) All Unicxde whitespace + if unicode.IsSpace(r) { + return true + } + // 2) Control characters (Cc) + if unicode.IsControl(r) { + return true + } + // 3) invisible/format chars + // https://github.com/laravel/framework/blob/7796b9b4f27a4c1bc1a5f5ae4a923ea5595fbb93/src/Illuminate/Support/Str.php#L32 + switch r { + // Soft hyphen, combining grapheme joiner, Arabic mark, Hangul fillers, Khmer indep. vowels, Mongolian, etc. + case 0x00AD, 0x034F, 0x061C, 0x115F, 0x1160, 0x17B4, 0x17B5, 0x180E, + // General Punctuation & Format (ZWSP, ZWNJ, ZWJ, LRM/RLM, NNBSP, Word Joiner, etc.) + 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007, 0x2008, 0x2009, 0x200A, + 0x200B, 0x200C, 0x200D, 0x200E, 0x200F, 0x202F, 0x205F, 0x2060, 0x2061, 0x2062, 0x2063, 0x2064, + 0x2065, 0x206A, 0x206B, 0x206C, 0x206D, 0x206E, 0x206F, + // Braille blank, Ideographic space, Hangul Filler, BOM, NBSP variants + 0x2800, 0x3000, 0x3164, 0xFEFF, 0xFFA0, + // Musical symbol format-ish + 0x1D159, 0x1D173, 0x1D174, 0x1D175, 0x1D176, 0x1D177, 0x1D178, 0x1D179, 0x1D17A, + // Tag Space (Plane 14) + 0xE0020: + return true + } + return false +} diff --git a/lib/request-processor/helpers/trimInvisible_test.go b/lib/request-processor/helpers/trimInvisible_test.go new file mode 100644 index 000000000..55b7ca222 --- /dev/null +++ b/lib/request-processor/helpers/trimInvisible_test.go @@ -0,0 +1,25 @@ +package helpers + +import "testing" + +func TestTrimInvisible(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"", ""}, + {" ls -la \t", "ls -la"}, + {"ls -la\u0000", "ls -la"}, + {"\u0000ls -la", "ls -la"}, + {"\u0000ls -la\u0000", "ls -la"}, + {"ls \u0000-la\u0000", "ls \u0000-la"}, + {"\u0020 \u0020", ""}, + } + + for _, test := range tests { + result := TrimInvisible(test.input) + if result != test.expected { + t.Errorf("TrimInvisible(%q) = %q; want %q", test.input, result, test.expected) + } + } +} diff --git a/lib/request-processor/helpers/tryParseURL.go b/lib/request-processor/helpers/tryParseURL.go index 648b59074..b1323de0a 100644 --- a/lib/request-processor/helpers/tryParseURL.go +++ b/lib/request-processor/helpers/tryParseURL.go @@ -1,14 +1,16 @@ package helpers import ( + "net" "net/url" + "strconv" "golang.org/x/net/idna" ) func TryParseURL(input string) *url.URL { - parsedURL, err := url.ParseRequestURI(input) - if err != nil { + parsedURL, err := url.Parse(input) + if err != nil || parsedURL.Host == "" { return nil } @@ -17,5 +19,26 @@ func TryParseURL(input string) *url.URL { if err == nil { parsedURL.Host = parsedHost } + // If the port is not present, we need to add it based on the scheme + if parsedURL.Port() == "" { + port := 0 + switch parsedURL.Scheme { + case "http": + port = 80 + case "https": + port = 443 + } + parsedURL.Host = parsedURL.Host + ":" + strconv.Itoa(int(port)) + } + host, port, err := net.SplitHostPort(parsedURL.Host) + if err == nil { + ip := net.ParseIP(host) + if ip != nil { + parsedURL.Host = ip.String() + ":" + port + } else { + parsedURL.Host = host + ":" + port + } + } + return parsedURL } diff --git a/lib/request-processor/helpers/tryParseURL_test.go b/lib/request-processor/helpers/tryParseURL_test.go index 940a57ec6..23e6bfde5 100644 --- a/lib/request-processor/helpers/tryParseURL_test.go +++ b/lib/request-processor/helpers/tryParseURL_test.go @@ -14,7 +14,7 @@ func TestTryParseURL_InvalidURL(t *testing.T) { } func TestTryParseURL_ValidURL(t *testing.T) { - input := "https://example.com" + input := "https://example.com:443" expected, _ := url.Parse(input) result := TryParseURL(input) diff --git a/lib/request-processor/utils/utils.go b/lib/request-processor/utils/utils.go index ba28fede1..4e92575a9 100644 --- a/lib/request-processor/utils/utils.go +++ b/lib/request-processor/utils/utils.go @@ -214,17 +214,56 @@ func IsIpBypassed(ip string) bool { } func getIpFromXForwardedFor(value string) string { - forwardedIps := strings.Split(value, ",") - for _, ip := range forwardedIps { - ip = strings.TrimSpace(ip) - if strings.Contains(ip, ":") { - parts := strings.Split(ip, ":") - if len(parts) == 2 { - ip = parts[0] + if strings.TrimSpace(value) == "" { + return "" + } + + parts := strings.Split(value, ",") + for i := range parts { + ip := strings.TrimSpace(parts[i]) + + // If it's already a valid IP (prevents splitting on ':' for IPv6) + if isIP(ip) { + parts[i] = ip + continue + } + + // Normalize bracketed IPv6 without port: "[2001:db8::1]" -> "2001:db8::1" + if strings.HasPrefix(ip, "[") && strings.HasSuffix(ip, "]") { + ip = ip[1 : len(ip)-1] + parts[i] = ip + continue + } + + // IPv6 with port: "[2001:db8::1]:443" -> "2001:db8::1" + if strings.HasPrefix(ip, "[") { + if idx := strings.Index(ip, "]:"); idx > 0 { + ip = ip[1:idx] + parts[i] = ip + continue } } - if isIP(ip) { - return ip + + // IPv4 with port: "203.0.113.5:1234" -> "203.0.113.5" + if strings.Count(ip, ":") == 1 { + if host, _, err := net.SplitHostPort(ip); err == nil { + ip = host + parts[i] = ip + continue + } + } + + // Leave as-is; will validate below + parts[i] = ip + } + + // Pick the first valid, non-private IP + for _, cand := range parts { + if !isIP(cand) { + continue + } + if !helpers.IsPrivateIP(cand) { + return cand } } return "" diff --git a/lib/request-processor/utils/utils_test.go b/lib/request-processor/utils/utils_test.go index c922a87ef..b7d9bb15c 100644 --- a/lib/request-processor/utils/utils_test.go +++ b/lib/request-processor/utils/utils_test.go @@ -506,3 +506,129 @@ func TestIsIpv6NotBlockedByIp(t *testing.T) { t.Errorf("expected false, got %v", result) } } + +func TestGetIpFromRequest(t *testing.T) { + //no headers and no remote address + globals.EnvironmentConfig.TrustProxy = false + if got := GetIpFromRequest("", ""); got != "" { + t.Errorf("expected empty, got %q", got) + } + + globals.EnvironmentConfig.TrustProxy = true + if got := GetIpFromRequest("", ""); got != "" { + t.Errorf("expected empty, got %q", got) + } + + //no headers and remote address + globals.EnvironmentConfig.TrustProxy = false + if got := GetIpFromRequest("1.2.3.4", ""); got != "1.2.3.4" { + t.Errorf("expected 1.2.3.4, got %q", got) + } + + globals.EnvironmentConfig.TrustProxy = true + if got := GetIpFromRequest("1.2.3.4", ""); got != "1.2.3.4" { + t.Errorf("expected 1.2.3.4, got %q", got) + } + + // x-forwarded-for without trust proxy + globals.EnvironmentConfig.TrustProxy = false + if got := GetIpFromRequest("1.2.3.4", "9.9.9.9"); got != "1.2.3.4" { + t.Errorf("expected 1.2.3.4, got %q", got) + } + + if got := GetIpFromRequest("df89:84af:85e0:c55f:960c:341a:2cc6:734d", "a3ad:8f95:d2a8:454b:cf19:be6e:73c6:f880"); got != "df89:84af:85e0:c55f:960c:341a:2cc6:734d" { + t.Errorf("expected df89:84af:85e0:c55f:960c:341a:2cc6:734d, got %q", got) + } + + // x-forwarded-for with trust proxy and "x-forwarded-for" is not an IP + globals.EnvironmentConfig.TrustProxy = true + if got := GetIpFromRequest("1.2.3.4", "invalid"); got != "1.2.3.4" { + t.Errorf("expected 1.2.3.4, got %q", got) + } + + // x-forwarded-for with trust proxy and IP contains port + globals.EnvironmentConfig.TrustProxy = true + if got := GetIpFromRequest("1.2.3.4", "9.9.9.9:8080"); got != "9.9.9.9" { + t.Errorf("expected 9.9.9.9, got %q", got) + } + if got := GetIpFromRequest("1.2.3.4", "[a3ad:8f95:d2a8:454b:cf19:be6e:73c6:f880]:8080"); got != "a3ad:8f95:d2a8:454b:cf19:be6e:73c6:f880" { + t.Errorf("expected a3ad:8f95:d2a8:454b:cf19:be6e:73c6:f880, got %q", got) + } + if got := GetIpFromRequest("1.2.3.4", "[a3ad:8f95:d2a8:454b:cf19:be6e:73c6:f880]"); got != "a3ad:8f95:d2a8:454b:cf19:be6e:73c6:f880" { + t.Errorf("expected a3ad:8f95:d2a8:454b:cf19:be6e:73c6:f880, got %q", got) + } + // Invalid format + if got := GetIpFromRequest("df89:84af:85e0:c55f:960c:341a:2cc6:734d", "a3ad:8f95:d2a8:454b:cf19:be6e:73c6:f880:8080"); got != "df89:84af:85e0:c55f:960c:341a:2cc6:734d" { + t.Errorf("expected df89:84af:85e0:c55f:960c:341a:2cc6:734d, got %q", got) + } + + // with trailing comma + globals.EnvironmentConfig.TrustProxy = true + if got := GetIpFromRequest("1.2.3.4", "9.9.9.9,"); got != "9.9.9.9" { + t.Errorf("expected 9.9.9.9, got %q", got) + } + if got := GetIpFromRequest("1.2.3.4", ",9.9.9.9"); got != "9.9.9.9" { + t.Errorf("expected 9.9.9.9, got %q", got) + } + if got := GetIpFromRequest("1.2.3.4", ",9.9.9.9,"); got != "9.9.9.9" { + t.Errorf("expected 9.9.9.9, got %q", got) + } + if got := GetIpFromRequest("1.2.3.4", ",9.9.9.9,,"); got != "9.9.9.9" { + t.Errorf("expected 9.9.9.9, got %q", got) + } + + // x-forwarded-for with trust proxy and "x-forwarded-for" is a private IP + globals.EnvironmentConfig.TrustProxy = true + if got := GetIpFromRequest("1.2.3.4", "127.0.0.1"); got != "1.2.3.4" { + t.Errorf("expected 1.2.3.4, got %q", got) + } + if got := GetIpFromRequest("df89:84af:85e0:c55f:960c:341a:2cc6:734d", "::1"); got != "df89:84af:85e0:c55f:960c:341a:2cc6:734d" { + t.Errorf("expected df89:84af:85e0:c55f:960c:341a:2cc6:734d, got %q", got) + } + + // x-forwarded-for with trust proxy and "x-forwarded-for" contains private IP + globals.EnvironmentConfig.TrustProxy = true + if got := GetIpFromRequest("1.2.3.4", "127.0.0.1, 9.9.9.9"); got != "9.9.9.9" { + t.Errorf("expected 9.9.9.9, got %q", got) + } + if got := GetIpFromRequest("df89:84af:85e0:c55f:960c:341a:2cc6:734d", "::1, a3ad:8f95:d2a8:454b:cf19:be6e:73c6:f880"); got != "a3ad:8f95:d2a8:454b:cf19:be6e:73c6:f880" { + t.Errorf("expected a3ad:8f95:d2a8:454b:cf19:be6e:73c6:f880, got %q", got) + } + + // x-forwarded-for with trust proxy and "x-forwarded-for" is public IP + globals.EnvironmentConfig.TrustProxy = true + if got := GetIpFromRequest("1.2.3.4", "9.9.9.9"); got != "9.9.9.9" { + t.Errorf("expected 9.9.9.9, got %q", got) + } + if got := GetIpFromRequest("df89:84af:85e0:c55f:960c:341a:2cc6:734d", "a3ad:8f95:d2a8:454b:cf19:be6e:73c6:f880"); got != "a3ad:8f95:d2a8:454b:cf19:be6e:73c6:f880" { + t.Errorf("expected a3ad:8f95:d2a8:454b:cf19:be6e:73c6:f880, got %q", got) + } + + // x-forwarded-for with trust proxy and "x-forwarded-for" contains private IP at the end + globals.EnvironmentConfig.TrustProxy = true + if got := GetIpFromRequest("1.2.3.4", "9.9.9.9, 127.0.0.1"); got != "9.9.9.9" { + t.Errorf("expected 9.9.9.9, got %q", got) + } + if got := GetIpFromRequest("df89:84af:85e0:c55f:960c:341a:2cc6:734d", "a3ad:8f95:d2a8:454b:cf19:be6e:73c6:f880, ::1"); got != "a3ad:8f95:d2a8:454b:cf19:be6e:73c6:f880" { + t.Errorf("expected a3ad:8f95:d2a8:454b:cf19:be6e:73c6:f880, got %q", got) + } + + // x-forwarded-for with trust proxy and multiple IPs + globals.EnvironmentConfig.TrustProxy = true + if got := GetIpFromRequest("1.2.3.4", "9.9.9.9, 8.8.8.8, 7.7.7.7"); got != "9.9.9.9" { + t.Errorf("expected 9.9.9.9, got %q", got) + } + if got := GetIpFromRequest("df89:84af:85e0:c55f:960c:341a:2cc6:734d", "a3ad:8f95:d2a8:454b:cf19:be6e:73c6:f880, 3b07:2fba:0270:2149:5fc1:2049:5f04:2131, 791d:967e:428a:90b9:8f6f:4fcc:5d88:015d"); got != "a3ad:8f95:d2a8:454b:cf19:be6e:73c6:f880" { + t.Errorf("expected a3ad:8f95:d2a8:454b:cf19:be6e:73c6:f880, got %q", got) + } + + // x-forwarded-for with trust proxy and many IPs + globals.EnvironmentConfig.TrustProxy = true + if got := GetIpFromRequest("1.2.3.4", "127.0.0.1, 192.168.0.1, 192.168.0.2, 9.9.9.9"); got != "9.9.9.9" { + t.Errorf("expected 9.9.9.9, got %q", got) + } + if got := GetIpFromRequest("1.2.3.4", "9.9.9.9, 127.0.0.1, 192.168.0.1, 192.168.0.2"); got != "9.9.9.9" { + t.Errorf("expected 9.9.9.9, got %q", got) + } + +} diff --git a/lib/request-processor/vulnerabilities/path-traversal/checkContextForPathTraversal.go b/lib/request-processor/vulnerabilities/path-traversal/checkContextForPathTraversal.go index b6f8a4860..22f02e310 100644 --- a/lib/request-processor/vulnerabilities/path-traversal/checkContextForPathTraversal.go +++ b/lib/request-processor/vulnerabilities/path-traversal/checkContextForPathTraversal.go @@ -2,17 +2,21 @@ package path_traversal import ( "main/context" + "main/helpers" "main/utils" "strings" ) func CheckContextForPathTraversal(filename string, operation string, checkPathStart bool) *utils.InterceptorResult { + trimmedFilename := helpers.TrimInvisible(filename) + sanitizedPath := SanitizePath(trimmedFilename) + for _, source := range context.SOURCES { mapss := source.CacheGet() - sanitizedPath := SanitizePath(filename) for str, path := range mapss { - inputString := SanitizePath(str) + trimmedInputString := helpers.TrimInvisible(str) + inputString := SanitizePath(trimmedInputString) if detectPathTraversal(sanitizedPath, inputString, checkPathStart) { return &utils.InterceptorResult{ Operation: operation, diff --git a/lib/request-processor/vulnerabilities/shell-injection/checkContextForShellInjection.go b/lib/request-processor/vulnerabilities/shell-injection/checkContextForShellInjection.go index ef3f24b67..3c971cec9 100644 --- a/lib/request-processor/vulnerabilities/shell-injection/checkContextForShellInjection.go +++ b/lib/request-processor/vulnerabilities/shell-injection/checkContextForShellInjection.go @@ -2,15 +2,18 @@ package shell_injection import ( "main/context" + "main/helpers" "main/utils" ) func CheckContextForShellInjection(command string, operation string) *utils.InterceptorResult { + trimmedCommand := helpers.TrimInvisible(command) for _, source := range context.SOURCES { mapss := source.CacheGet() for str, path := range mapss { - if detectShellInjection(command, str) { + trimmedInputString := helpers.TrimInvisible(str) + if detectShellInjection(trimmedCommand, trimmedInputString) { return &utils.InterceptorResult{ Operation: operation, Kind: utils.Shell_injection, diff --git a/lib/request-processor/vulnerabilities/sql-injection/checkContextForSqlInjection.go b/lib/request-processor/vulnerabilities/sql-injection/checkContextForSqlInjection.go index c5d980e37..281a3ee03 100644 --- a/lib/request-processor/vulnerabilities/sql-injection/checkContextForSqlInjection.go +++ b/lib/request-processor/vulnerabilities/sql-injection/checkContextForSqlInjection.go @@ -2,6 +2,7 @@ package sql_injection import ( "main/context" + "main/helpers" "main/utils" ) @@ -10,13 +11,15 @@ import ( * if it's a possible SQL Injection, if so the function returns an InterceptorResult */ func CheckContextForSqlInjection(sql string, operation string, dialect string) *utils.InterceptorResult { + trimmedSql := helpers.TrimInvisible(sql) dialectId := utils.GetSqlDialectFromString(dialect) for _, source := range context.SOURCES { mapss := source.CacheGet() for str, path := range mapss { - if detectSQLInjection(sql, str, dialectId) { + trimmedInputString := helpers.TrimInvisible(str) + if detectSQLInjection(trimmedSql, trimmedInputString, dialectId) { return &utils.InterceptorResult{ Operation: operation, Kind: utils.Sql_injection, diff --git a/lib/request-processor/vulnerabilities/ssrf/checkContextForSSRF.go b/lib/request-processor/vulnerabilities/ssrf/checkContextForSSRF.go index 7bc023404..06faa6347 100644 --- a/lib/request-processor/vulnerabilities/ssrf/checkContextForSSRF.go +++ b/lib/request-processor/vulnerabilities/ssrf/checkContextForSSRF.go @@ -2,17 +2,19 @@ package ssrf import ( "main/context" + "main/helpers" "main/utils" ) /* This is called before a request is made to check for SSRF and block the request (not execute it) if SSRF found */ func CheckContextForSSRF(hostname string, port uint32, operation string) *utils.InterceptorResult { + trimmedHostname := helpers.TrimInvisible(hostname) for _, source := range context.SOURCES { mapss := source.CacheGet() for str, path := range mapss { - - if findHostnameInUserInput(str, hostname, port) { + trimmedInputString := helpers.TrimInvisible(str) + if findHostnameInUserInput(trimmedInputString, trimmedHostname, port) { interceptorResult := utils.InterceptorResult{ Operation: operation, Kind: utils.Ssrf, @@ -22,13 +24,13 @@ func CheckContextForSSRF(hostname string, port uint32, operation string) *utils. Payload: str, } - if containsPrivateIPAddress(hostname) { + if containsPrivateIPAddress(trimmedHostname) { // Hostname was found in user input and is actually a private IP address (http://127.0.0.1) -> SSRF interceptorResult.Metadata["isPrivateIp"] = "true" return &interceptorResult } - resolvedIpStatus := getResolvedIpStatusForHostname(hostname) + resolvedIpStatus := getResolvedIpStatusForHostname(trimmedHostname) if resolvedIpStatus != nil { interceptorResult.Metadata["resolvedIp"] = resolvedIpStatus.ip if resolvedIpStatus.isIMDS { @@ -74,7 +76,7 @@ func CheckEffectiveHostnameForSSRF(effectiveHostname string) *utils.InterceptorR } } - return nil + return interceptorResult } /* This is called after the request is made to check for SSRF in the resolvedIP - IP optained from the PHP library that made the request (curl) */ diff --git a/lib/request-processor/vulnerabilities/ssrf/findHostnameInUserInput.go b/lib/request-processor/vulnerabilities/ssrf/findHostnameInUserInput.go index 24eb55307..f5c4d90ac 100644 --- a/lib/request-processor/vulnerabilities/ssrf/findHostnameInUserInput.go +++ b/lib/request-processor/vulnerabilities/ssrf/findHostnameInUserInput.go @@ -2,28 +2,50 @@ package ssrf import ( "main/helpers" + "main/log" + "net/url" + "strconv" "strings" ) -func findHostnameInUserInput(userInput string, hostname string, port uint32) bool { +func getVariants(userInput string) []string { + variants := []string{userInput, "http://" + userInput, "https://" + userInput} + decodedUserInput, err := url.QueryUnescape(userInput) + if err == nil && decodedUserInput != userInput { + variants = append(variants, decodedUserInput, "http://"+decodedUserInput, "https://"+decodedUserInput) + } + return variants +} +func findHostnameInUserInput(userInput string, hostname string, port uint32) bool { + log.Debugf("findHostnameInUserInput: userInput: %s, hostname: %s, port: %d", userInput, hostname, port) if len(userInput) <= 1 { return false } + // if hostname contains : we need to add the [ and ] to the hostname (ipv6) + if strings.Contains(hostname, ":") { + hostname = "[" + hostname + "]" + } - hostnameURL := helpers.TryParseURL("http://" + hostname) + hostnameURL := helpers.TryParseURL("http://" + hostname + ":" + strconv.Itoa(int(port))) if hostnameURL == nil { return false } userInput = helpers.ExtractResourceOrOriginal(userInput) - variants := []string{userInput, "http://" + userInput, "https://" + userInput} + userInput = helpers.NormalizeRawUrl(userInput) + + variants := getVariants(userInput) for _, variant := range variants { userInputURL := helpers.TryParseURL(variant) + if userInputURL == nil { + continue + } + // https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2 // "The host subcomponent is case-insensitive." - if userInputURL != nil && strings.EqualFold(userInputURL.Hostname(), hostnameURL.Hostname()) { + if strings.EqualFold(userInputURL.Hostname(), hostnameURL.Hostname()) { userPort := helpers.GetPortFromURL(userInputURL) if port == 0 { diff --git a/lib/request-processor/vulnerabilities/ssrf/findHostnameInUserInput_test.go b/lib/request-processor/vulnerabilities/ssrf/findHostnameInUserInput_test.go index 17e75ee0c..91229bb9b 100644 --- a/lib/request-processor/vulnerabilities/ssrf/findHostnameInUserInput_test.go +++ b/lib/request-processor/vulnerabilities/ssrf/findHostnameInUserInput_test.go @@ -11,6 +11,25 @@ func TestFindHostnameInUserInput(t *testing.T) { port uint32 expected bool }{ + {"aa:@localhost:8080", "localhost", 8080, true}, + {"http://127.1.1.1:4000∖@127.0.0.1:80/", "127.0.0.1", 80, true}, + {"http://127.1.1.1:4000\\\\@127.0.0.1:8080/", "127.0.0.1", 8080, true}, + {"http://[0:0:0:0:0:ffff:127.0.0.1]/thefile", "0:0:0:0:0:ffff:127.0.0.1", 80, true}, + {"http://[0000:0000:0000:0000:0000:0000:0000:0001]:4000", "0000:0000:0000:0000:0000:0000:0000:0001", 4000, true}, + {"http://127.0.0.1:8080#\\@127.2.2.2:80/ ", "127.0.0.1", 8080, true}, + {"http://1.1.1.1 &@127.0.0.1:4000# @3.3.3.3/", "127.0.0.1", 4000, true}, + {"http://127.1.1.1:4000:\\@@127.0.0.1:8080/", "127.0.0.1", 8080, true}, + {"http://127.1.1.1:4000\\@127.0.0.1:8080/", "127.0.0.1", 8080, true}, + {"http://127.0.0.1:8080/@/127.1.1.1:4000", "127.0.0.1", 8080, true}, + {"http://%31%32%37.%30.%30.%31:4000", "127.0.0.1", 4000, true}, + {"http://%30:4000", "0", 4000, true}, + {"http://127%2E0%2E0%2E1:4000", "127.0.0.1", 4000, true}, + {"http://[::ffff:127.0.0.1]:4000", "::ffff:127.0.0.1", 4000, true}, + {"http://[0:0:0:0:0:0:0:1]:4000", "::1", 4000, true}, + {"http://[::]:4000", "::", 4000, true}, + {"http://[0000:0000:0000:0000:0000:0000:0000:0001]:4000", "::1", 4000, true}, + {"http://[::1]:4000", "::1", 4000, true}, + {"http://[0:0::1]:4000", "::1", 4000, true}, {"https://m%C3%BCnchen.de", "münchen.de", 0, true}, {"https://münchen.de", "xn--mnchen-3ya.de", 0, true}, {"https://xn--mnchen-3ya.de", "münchen.de", 0, true}, diff --git a/package/rpm/aikido.spec b/package/rpm/aikido.spec index ade4a4b83..fdf0822e8 100644 --- a/package/rpm/aikido.spec +++ b/package/rpm/aikido.spec @@ -1,5 +1,5 @@ Name: aikido-php-firewall -Version: 1.3.4 +Version: 1.3.5 Release: 1 Summary: Aikido PHP Extension License: GPL diff --git a/tests/cli/shell_injection/test_exec_trim.phpt b/tests/cli/shell_injection/test_exec_trim.phpt new file mode 100644 index 000000000..7240cbf3f --- /dev/null +++ b/tests/cli/shell_injection/test_exec_trim.phpt @@ -0,0 +1,23 @@ +--TEST-- +Test PHP shell injection (exec) trim + +--ENV-- +AIKIDO_LOG_LEVEL=INFO +AIKIDO_BLOCK=1 + +--POST-- +test=www.example`whoami`.com + +--FILE-- + + +--EXPECTREGEX-- +.*Fatal error: Uncaught Exception: Aikido firewall has blocked a shell injection.* \ No newline at end of file diff --git a/tests/cli/ssrf/test_ssrf_curl_exec_redirect_fp.phpt b/tests/cli/ssrf/test_ssrf_curl_exec_redirect_fp.phpt new file mode 100644 index 000000000..ac7196027 --- /dev/null +++ b/tests/cli/ssrf/test_ssrf_curl_exec_redirect_fp.phpt @@ -0,0 +1,70 @@ +--TEST-- +Ensure cURL requests to example.com and after to a local dev server (127.0.0.1) are not incorrectly blocked as SSRF by Aikido + +--ENV-- +AIKIDO_LOG_LEVEL=DEBUG +AIKIDO_BLOCK=1 + +--POST-- +test=https://example.com + +--FILE-- + ['pipe', 'r'], + 1 => ['file', '/dev/null', 'a'], + 2 => ['file', '/dev/null', 'a'] +]; + +try { + // Start PHP server + $process = proc_open("php -S $host:$port", $descriptorspec, $pipes); + if (!is_resource($process)) { + throw new RuntimeException("Failed to start PHP server."); + } + + $status = proc_get_status($process); + $pid = $status['pid']; + + // Wait a moment to ensure server starts + sleep(1); + + // Perform the cURL request + $ch1 = curl_init("https://example.com"); + curl_setopt($ch1, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch1, CURLOPT_FOLLOWLOCATION, true); + + $response = curl_exec($ch1); + + if ($response === false) { + echo "cURL Error: " . curl_error($ch); + } else { + echo "Response: " . $response; + } + + $ch2 = curl_init("http://127.0.0.1:3000"); + curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch2, CURLOPT_FOLLOWLOCATION, true); + $response2 = curl_exec($ch2); + + +} catch (Throwable $e) { + echo "Error: " . $e->getMessage() . "\n"; +} finally { + // Ensure the server is killed if started + if ($pid) { + exec("kill -9 $pid"); + } + if (isset($process) && is_resource($process)) { + proc_close($process); + } +} + + +--EXPECTREGEX-- +(?s)\A(?!.*Aikido firewall has blocked a server-side request forgery).*?\z diff --git a/tests/cli/ssrf/test_ssrf_obfuscated_host.phpt b/tests/cli/ssrf/test_ssrf_obfuscated_host.phpt new file mode 100644 index 000000000..519195396 --- /dev/null +++ b/tests/cli/ssrf/test_ssrf_obfuscated_host.phpt @@ -0,0 +1,62 @@ +--TEST-- +Test ssrf - obfuscated host + +--ENV-- +AIKIDO_LOG_LEVEL=INFO +AIKIDO_BLOCK=1 + +--POST-- +test=http://127.1.1.1:4000\@127.0.0.1:4000 + +--FILE-- + ['pipe', 'r'], + 1 => ['file', '/dev/null', 'a'], + 2 => ['file', '/dev/null', 'a'] +]; + +try { + // Start PHP server + $process = proc_open("php -S $host:$port", $descriptorspec, $pipes); + if (!is_resource($process)) { + throw new RuntimeException("Failed to start PHP server."); + } + + $status = proc_get_status($process); + $pid = $status['pid']; + + // Wait a moment to ensure server starts + sleep(1); + + // Perform the cURL request + $ch1 = curl_init("http://127.0.0.1:4000"); + curl_setopt($ch1, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch1, CURLOPT_FOLLOWLOCATION, true); + $response = curl_exec($ch1); + curl_close($ch1); + + echo "Response:\n$response\n"; + +} catch (Throwable $e) { + echo "Error: " . $e->getMessage() . "\n"; +} finally { + // Ensure the server is killed if started + if ($pid) { + exec("kill -9 $pid"); + } + if (isset($process) && is_resource($process)) { + proc_close($process); + } +} + + + + +--EXPECTREGEX-- +.*Aikido firewall has blocked a server-side request forgery.*