From 1a7cfc791903e9184d25c983f72df0325047db6b Mon Sep 17 00:00:00 2001 From: Easton97-Jens <66330090+Easton97-Jens@users.noreply.github.com> Date: Mon, 4 May 2026 17:20:01 +0200 Subject: [PATCH] Add files via upload Add files via upload Fix AFL fuzzer input handling and null operator check Fix fuzzer API usage for operators and transformations Update fuzzing workflow to use ubuntu-latest Update runtime-sanitizers.yml Update codeql-security.yml --- .github/workflows/codeql-security.yml | 85 +++++++++++++ .github/workflows/fuzzing-smoke.yml | 146 ++++++++++++++++++++++ .github/workflows/runtime-sanitizers.yml | 152 +++++++++++++++++++++++ test/fuzzer/afl_fuzzer.cc | 82 ++++++------ 4 files changed, 422 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/codeql-security.yml create mode 100644 .github/workflows/fuzzing-smoke.yml create mode 100644 .github/workflows/runtime-sanitizers.yml diff --git a/.github/workflows/codeql-security.yml b/.github/workflows/codeql-security.yml new file mode 100644 index 0000000000..61a9fe2fe5 --- /dev/null +++ b/.github/workflows/codeql-security.yml @@ -0,0 +1,85 @@ +name: CodeQL Security + +on: + push: + pull_request: + workflow_dispatch: + schedule: + - cron: "0 3 * * 1" + +permissions: + contents: read + security-events: write + actions: read + +jobs: + codeql: + name: CodeQL C/C++ + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + submodules: recursive + + - name: Detect latest Lua dev package + id: detect_lua + shell: bash + run: | + set -euo pipefail + sudo apt-get update -y -qq + CANDIDATES="$(apt-cache pkgnames | grep -E '^liblua[0-9]+\.[0-9]+-dev$' || true)" + + if [ -z "$CANDIDATES" ]; then + echo "No libluaX.Y-dev package found" + exit 1 + fi + + BEST_PKG="$( + printf '%s\n' "$CANDIDATES" \ + | sed -E 's/^liblua([0-9]+\.[0-9]+)-dev$/\1 &/' \ + | sort -V \ + | tail -n1 \ + | awk '{print $2}' + )" + + echo "lua_pkg=$BEST_PKG" >> "$GITHUB_OUTPUT" + + - name: Install dependencies + run: | + sudo apt-get install -y \ + autoconf \ + automake \ + build-essential \ + libtool \ + pkg-config \ + libyajl-dev \ + libcurl4-openssl-dev \ + liblmdb-dev \ + ${{ steps.detect_lua.outputs.lua_pkg }} \ + libmaxminddb-dev \ + libpcre2-dev \ + libxml2-dev \ + libfuzzy-dev \ + pcre2-utils \ + libpcre3-dev \ + bison \ + flex \ + python3 \ + python3-venv + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: c-cpp + queries: security-extended,security-and-quality + + - name: Build for CodeQL database + run: | + ./build.sh + ./configure --enable-assertions=yes + make -j"$(nproc)" + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/fuzzing-smoke.yml b/.github/workflows/fuzzing-smoke.yml new file mode 100644 index 0000000000..cee95ff064 --- /dev/null +++ b/.github/workflows/fuzzing-smoke.yml @@ -0,0 +1,146 @@ +name: Fuzzing Smoke Test + +on: + workflow_dispatch: + inputs: + run_minutes: + description: "How many minutes AFL++ should fuzz" + required: false + default: "10" + fail_on_hangs: + description: "Fail workflow when AFL++ reports hangs" + required: false + default: "false" + type: choice + options: + - "false" + - "true" + schedule: + - cron: "0 2 * * 0" + +permissions: + contents: read + +concurrency: + group: fuzzing-smoke-${{ github.ref }} + cancel-in-progress: false + +jobs: + fuzzing-smoke: + name: AFL++ fuzzing smoke test + runs-on: ubuntu-latest + timeout-minutes: 60 + + env: + AFL_SKIP_CPUFREQ: "1" + AFL_NO_AFFINITY: "1" + AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES: "1" + AFL_NO_UI: "1" + AFL_FAST_CAL: "1" + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + submodules: recursive + + - name: Detect latest Lua packages + id: detect_lua + shell: bash + run: | + set -euo pipefail + + sudo apt-get update -y -qq + + CANDIDATES="$(apt-cache pkgnames | grep -E '^liblua[0-9]+\.[0-9]+-dev$' || true)" + + if [ -z "$CANDIDATES" ]; then + echo "No libluaX.Y-dev package found" + exit 1 + fi + + BEST_PKG="$( + printf '%s\n' "$CANDIDATES" \ + | sed -E 's/^liblua([0-9]+\.[0-9]+)-dev$/\1 &/' \ + | sort -V \ + | tail -n1 \ + | awk '{print $2}' + )" + + BEST_VER="$(printf '%s\n' "$BEST_PKG" | sed -E 's/^liblua([0-9]+\.[0-9]+)-dev$/\1/')" + LUA_PKG="lua$BEST_VER" + + echo "lua_dev_pkg=$BEST_PKG" >> "$GITHUB_OUTPUT" + echo "lua_pkg=$LUA_PKG" >> "$GITHUB_OUTPUT" + + - name: Install dependencies + run: | + sudo apt-get install -y \ + autoconf automake build-essential afl++ clang libtool pkg-config \ + libyajl-dev libcurl4-openssl-dev liblmdb-dev \ + ${{ steps.detect_lua.outputs.lua_dev_pkg }} \ + ${{ steps.detect_lua.outputs.lua_pkg }} \ + libmaxminddb-dev libpcre2-dev libxml2-dev libfuzzy-dev \ + pcre2-utils libpcre3-dev bison flex python3 python3-venv + + - name: Build ModSecurity with AFL++ + env: + CC: afl-clang-fast + CXX: afl-clang-fast++ + run: | + ./build.sh + ./configure --enable-afl-fuzz --enable-parser-generation --enable-assertions=yes + make -j"$(nproc)" + + - name: Locate AFL target + id: target + run: | + for f in ./test/fuzzer/afl_fuzzer ./test/fuzzer/.libs/afl_fuzzer; do + [ -x "$f" ] && echo "target=$f" >> $GITHUB_OUTPUT && exit 0 + done + echo "Fuzzer not found" && exit 1 + + - name: Create seed corpus + run: | + rm -rf fuzz-in fuzz-out + mkdir -p fuzz-in fuzz-out + printf '' > fuzz-in/empty + printf 'abc' > fuzz-in/plain + + - name: Dry-run + run: timeout 10s "${{ steps.target.outputs.target }}" < fuzz-in/plain + + - name: Run AFL++ + run: | + timeout "${{ github.event.inputs.run_minutes || '10' }}m" \ + afl-fuzz -i fuzz-in -o fuzz-out -m none -t 1000+ \ + -- "${{ steps.target.outputs.target }}" || true + + - name: Summarize + id: summary + run: | + CRASH=$(find fuzz-out -path '*/crashes/id:*' -type f | wc -l) + HANG=$(find fuzz-out -path '*/hangs/id:*' -type f | wc -l) + echo "crash_count=$CRASH" >> $GITHUB_OUTPUT + echo "hang_count=$HANG" >> $GITHUB_OUTPUT + + - name: Package results + if: always() + run: | + tar -czf afl-fuzz-results.tar.gz fuzz-in fuzz-out + + - name: Upload results + if: always() + uses: actions/upload-artifact@v7 + with: + name: afl-fuzz-results-${{ github.run_id }} + path: afl-fuzz-results.tar.gz + + - name: Fail on crashes + if: steps.summary.outputs.crash_count != '0' + run: exit 1 + + - name: Fail on hangs + if: github.event.inputs.fail_on_hangs == 'true' && steps.summary.outputs.hang_count != '0' + run: exit 1 diff --git a/.github/workflows/runtime-sanitizers.yml b/.github/workflows/runtime-sanitizers.yml new file mode 100644 index 0000000000..7fd7197c4e --- /dev/null +++ b/.github/workflows/runtime-sanitizers.yml @@ -0,0 +1,152 @@ +name: Runtime Sanitizers + +on: + push: + pull_request: + workflow_dispatch: + schedule: + - cron: "0 4 * * 1" + +jobs: + asan-ubsan-linux: + name: ASan/UBSan Linux + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + submodules: recursive + + - name: Detect latest Lua dev package + id: detect_lua + shell: bash + run: | + set -euo pipefail + sudo apt-get update -y -qq + CANDIDATES="$(apt-cache pkgnames | grep -E '^liblua[0-9]+\.[0-9]+-dev$' || true)" + + if [ -z "$CANDIDATES" ]; then + echo "No libluaX.Y-dev package found" + exit 1 + fi + + BEST_PKG="$( + printf '%s\n' "$CANDIDATES" \ + | sed -E 's/^liblua([0-9]+\.[0-9]+)-dev$/\1 &/' \ + | sort -V \ + | tail -n1 \ + | awk '{print $2}' + )" + + echo "lua_pkg=$BEST_PKG" >> "$GITHUB_OUTPUT" + + - name: Install dependencies + run: | + sudo apt-get install -y \ + autoconf \ + automake \ + build-essential \ + clang \ + libtool \ + pkg-config \ + libyajl-dev \ + libcurl4-openssl-dev \ + liblmdb-dev \ + ${{ steps.detect_lua.outputs.lua_pkg }} \ + libmaxminddb-dev \ + libpcre2-dev \ + libxml2-dev \ + libfuzzy-dev \ + pcre2-utils \ + libpcre3-dev \ + bison \ + flex \ + python3 \ + python3-venv + + - name: Build with AddressSanitizer and UndefinedBehaviorSanitizer + env: + CC: clang + CXX: clang++ + CFLAGS: "-fsanitize=address,undefined -fno-omit-frame-pointer -O1" + CXXFLAGS: "-fsanitize=address,undefined -fno-omit-frame-pointer -O1" + LDFLAGS: "-fsanitize=address,undefined" + ASAN_OPTIONS: "detect_leaks=1:abort_on_error=1:strict_string_checks=1" + UBSAN_OPTIONS: "halt_on_error=1:print_stacktrace=1" + run: | + ./build.sh + ./configure --enable-assertions=yes + make -j"$(nproc)" + timeout 30m make check + + valgrind-linux: + name: Valgrind Linux + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + submodules: recursive + + - name: Detect latest Lua dev package + id: detect_lua + shell: bash + run: | + set -euo pipefail + sudo apt-get update -y -qq + CANDIDATES="$(apt-cache pkgnames | grep -E '^liblua[0-9]+\.[0-9]+-dev$' || true)" + + if [ -z "$CANDIDATES" ]; then + echo "No libluaX.Y-dev package found" + exit 1 + fi + + BEST_PKG="$( + printf '%s\n' "$CANDIDATES" \ + | sed -E 's/^liblua([0-9]+\.[0-9]+)-dev$/\1 &/' \ + | sort -V \ + | tail -n1 \ + | awk '{print $2}' + )" + + echo "lua_pkg=$BEST_PKG" >> "$GITHUB_OUTPUT" + + - name: Install dependencies + run: | + sudo apt-get install -y \ + autoconf \ + automake \ + build-essential \ + valgrind \ + libtool \ + pkg-config \ + libyajl-dev \ + libcurl4-openssl-dev \ + liblmdb-dev \ + ${{ steps.detect_lua.outputs.lua_pkg }} \ + libmaxminddb-dev \ + libpcre2-dev \ + libxml2-dev \ + libfuzzy-dev \ + pcre2-utils \ + libpcre3-dev \ + bison \ + flex \ + python3 \ + python3-venv + + - name: Build + run: | + ./build.sh + ./configure --enable-assertions=yes + make -j"$(nproc)" + + - name: Run tests under Valgrind + run: | + timeout 45m valgrind \ + --error-exitcode=1 \ + --leak-check=full \ + --show-leak-kinds=definite,indirect \ + make check diff --git a/test/fuzzer/afl_fuzzer.cc b/test/fuzzer/afl_fuzzer.cc index 93afdef276..9cff9160a6 100644 --- a/test/fuzzer/afl_fuzzer.cc +++ b/test/fuzzer/afl_fuzzer.cc @@ -20,7 +20,7 @@ #include "src/actions/transformations/transformation.h" /** - * for i in $(ls -l src/actions/transformations/*.h | awk {'print $9'}); do echo "#include \"$i\""; done; + * for i in $(ls -l src/actions/transformations/\*.h | awk {'print $9'}); do echo "#include \"$i\""; done; * */ #include "src/actions/transformations/base64_decode.h" @@ -64,7 +64,7 @@ /** - * for i in $(ls -l src/operators/*.h | awk {'print $9'}); do echo "#include \"$i\""; done; + * for i in $(ls -l src/operators/\*.h | awk {'print $9'}); do echo "#include \"$i\""; done; * */ #include "src/operators/begins_with.h" @@ -121,11 +121,20 @@ using namespace modsecurity; #endif #include #include +#include +#include + +#ifndef __AFL_LOOP +#define __AFL_LOOP(x) (1) +#endif inline void op_test(const std::string &opName, const std::string &s) { Operator *op = Operator::instantiate(opName, ""); + if (op == nullptr) { + return; + } op->init("", nullptr); - op->evaluate(nullptr, nullptr, s, nullptr); + op->evaluate(nullptr, nullptr, s); delete op; } @@ -142,7 +151,12 @@ int main(int argc, char** argv) { memset(buf, 0, 128); read_bytes = read(STDIN_FILENO, buf, 128); - std::string currentString = std::string(read_bytes, 128); + if (read_bytes <= 0) { + continue; + } + + std::string currentString = + std::string(reinterpret_cast(buf), read_bytes); const std::string& s = currentString; #if 0 std::string z = lastString; @@ -158,52 +172,34 @@ int main(int argc, char** argv) { /** * Transformations, generated by: * - * for i in $(grep "class " -Ri src/actions/transformations/* | grep " :" | grep -v "InstantCache" | awk {'print $2'}); do echo $i *$(echo $i | awk '{print tolower($0)}') = new $i\(\"$i\"\)\; $(echo $i | awk '{print tolower($0)}')-\>evaluate\(s, NULL\)\; delete $(echo $i | awk '{print tolower($0)}')\;; done; + * for i in $(grep "class " -Ri src/actions/transformations/\* | grep " :" | grep -v "InstantCache" | awk {'print $2'}); do echo $i *$(echo $i | awk '{print tolower($0)}') = new $i\(\"$i\"\)\; $(echo $i | awk '{print tolower($0)}')-\>evaluate\(s, NULL\)\; delete $(echo $i | awk '{print tolower($0)}')\;; done; * */ -Base64Decode *base64decode = new Base64Decode("Base64Decode"); base64decode->evaluate(s, NULL); delete base64decode; -Base64DecodeExt *base64decodeext = new Base64DecodeExt("Base64DecodeExt"); base64decodeext->evaluate(s, NULL); delete base64decodeext; -Base64Encode *base64encode = new Base64Encode("Base64Encode"); base64encode->evaluate(s, NULL); delete base64encode; -CmdLine *cmdline = new CmdLine("CmdLine"); cmdline->evaluate(s, NULL); delete cmdline; -CompressWhitespace *compresswhitespace = new CompressWhitespace("CompressWhitespace"); compresswhitespace->evaluate(s, NULL); delete compresswhitespace; -CssDecode *cssdecode = new CssDecode("CssDecode"); cssdecode->evaluate(s, NULL); delete cssdecode; -EscapeSeqDecode *escapeseqdecode = new EscapeSeqDecode("EscapeSeqDecode"); escapeseqdecode->evaluate(s, NULL); delete escapeseqdecode; -HexDecode *hexdecode = new HexDecode("HexDecode"); hexdecode->evaluate(s, NULL); delete hexdecode; -HexEncode *hexencode = new HexEncode("HexEncode"); hexencode->evaluate(s, NULL); delete hexencode; -HtmlEntityDecode *htmlentitydecode = new HtmlEntityDecode("HtmlEntityDecode"); htmlentitydecode->evaluate(s, NULL); delete htmlentitydecode; -JsDecode *jsdecode = new JsDecode("JsDecode"); jsdecode->evaluate(s, NULL); delete jsdecode; -Length *length = new Length("Length"); length->evaluate(s, NULL); delete length; -LowerCase *lowercase = new LowerCase("LowerCase"); lowercase->evaluate(s, NULL); delete lowercase; -Md5 *md5 = new Md5("Md5"); md5->evaluate(s, NULL); delete md5; -None *none = new None("None"); none->evaluate(s, NULL); delete none; -NormalisePath *normalisepath = new NormalisePath("NormalisePath"); normalisepath->evaluate(s, NULL); delete normalisepath; -NormalisePathWin *normalisepathwin = new NormalisePathWin("NormalisePathWin"); normalisepathwin->evaluate(s, NULL); delete normalisepathwin; -ParityEven7bit *parityeven7bit = new ParityEven7bit("ParityEven7bit"); parityeven7bit->evaluate(s, NULL); delete parityeven7bit; -ParityOdd7bit *parityodd7bit = new ParityOdd7bit("ParityOdd7bit"); parityodd7bit->evaluate(s, NULL); delete parityodd7bit; -ParityZero7bit *parityzero7bit = new ParityZero7bit("ParityZero7bit"); parityzero7bit->evaluate(s, NULL); delete parityzero7bit; -RemoveComments *removecomments = new RemoveComments("RemoveComments"); removecomments->evaluate(s, NULL); delete removecomments; -RemoveCommentsChar *removecommentschar = new RemoveCommentsChar("RemoveCommentsChar"); removecommentschar->evaluate(s, NULL); delete removecommentschar; -RemoveNulls *removenulls = new RemoveNulls("RemoveNulls"); removenulls->evaluate(s, NULL); delete removenulls; -RemoveWhitespace *removewhitespace = new RemoveWhitespace("RemoveWhitespace"); removewhitespace->evaluate(s, NULL); delete removewhitespace; -ReplaceComments *replacecomments = new ReplaceComments("ReplaceComments"); replacecomments->evaluate(s, NULL); delete replacecomments; -ReplaceNulls *replacenulls = new ReplaceNulls("ReplaceNulls"); replacenulls->evaluate(s, NULL); delete replacenulls; -Sha1 *sha1 = new Sha1("Sha1"); sha1->evaluate(s, NULL); delete sha1; -SqlHexDecode *sqlhexdecode = new SqlHexDecode("SqlHexDecode"); sqlhexdecode->evaluate(s, NULL); delete sqlhexdecode; -Transformation *transformation = new Transformation("Transformation"); transformation->evaluate(s, NULL); delete transformation; -Trim *trim = new Trim("Trim"); trim->evaluate(s, NULL); delete trim; -TrimLeft *trimleft = new TrimLeft("TrimLeft"); trimleft->evaluate(s, NULL); delete trimleft; -TrimRight *trimright = new TrimRight("TrimRight"); trimright->evaluate(s, NULL); delete trimright; -UpperCase *uppercase = new UpperCase("UpperCase"); uppercase->evaluate(s, NULL); delete uppercase; -UrlDecode *urldecode = new UrlDecode("UrlDecode"); urldecode->evaluate(s, NULL); delete urldecode; -UrlDecodeUni *urldecodeuni = new UrlDecodeUni("UrlDecodeUni"); urldecodeuni->evaluate(s, NULL); delete urldecodeuni; -UrlEncode *urlencode = new UrlEncode("UrlEncode"); urlencode->evaluate(s, NULL); delete urlencode; -Utf8ToUnicode *utf8tounicode = new Utf8ToUnicode("Utf8ToUnicode"); utf8tounicode->evaluate(s, NULL); delete utf8tounicode; +std::vector transformationNames = { + "Base64Decode", "Base64DecodeExt", "Base64Encode", "CmdLine", + "CompressWhitespace", "CssDecode", "EscapeSeqDecode", "HexDecode", + "HexEncode", "HtmlEntityDecode", "JsDecode", "Length", "LowerCase", + "Md5", "None", "NormalisePath", "NormalisePathWin", "ParityEven7bit", + "ParityOdd7bit", "ParityZero7bit", "RemoveComments", "RemoveCommentsChar", + "RemoveNulls", "RemoveWhitespace", "ReplaceComments", "ReplaceNulls", + "Sha1", "SqlHexDecode", "Transformation", "Trim", "TrimLeft", + "TrimRight", "UpperCase", "UrlDecode", "UrlDecodeUni", "UrlEncode", + "Utf8ToUnicode" +}; +for (const auto &name : transformationNames) { + std::unique_ptr transformation(Transformation::instantiate(name)); + if (transformation == nullptr) { + continue; + } + std::string value = s; + transformation->transform(value, t); +} /** * Operators, generated by: * - * for i in $(grep "class " -Ri src/operators/* | grep " :" | awk {'print $2'}); do echo $i *$(echo $i | awk '{print tolower($0)}') = new $i\(\"$i\", z, false\)\; $(echo $i | awk '{print tolower($0)}')-\>evaluate\(t, s\)\; delete $(echo $i | awk '{print tolower($0)}')\;; done; + * for i in $(grep "class " -Ri src/operators/\* | grep " :" | awk {'print $2'}); do echo $i *$(echo $i | awk '{print tolower($0)}') = new $i\(\"$i\", z, false\)\; $(echo $i | awk '{print tolower($0)}')-\>evaluate\(t, s\)\; delete $(echo $i | awk '{print tolower($0)}')\;; done; * */ op_test("BeginsWith", s);