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);