{"uuid": "0d7c9217-6d44-4974-806c-c26e3b9315ab", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-42945", "type": "confirmed", "source": "https://gist.github.com/MuktadirHassan/9e4dc13c0b88e804e365cf3d2bfeadb4", "content": "#!/usr/bin/env bash\n# CVE-2026-42945 (\"NGINX Rift\") checker\n#\n# Heuristic scan for:\n#   1. NGINX version in the advisory's affected range\n#   2. Vulnerable config pattern: a rewrite directive with an unnamed\n#      capture group ($1, $2, ...) and a \"?\" in the replacement,\n#      followed by another rewrite/if/set directive in the SAME block\n#\n# Affected (per F5 advisory K000161019):\n#   NGINX Open Source: 0.6.27 through 1.30.0 (fixed in 1.30.1, 1.31.0)\n#   NGINX Plus:        R32 through R36       (fixed in R32 P6, R36 P4)\n#\n# This is a heuristic. It can miss things and it can have false\n# positives. Treat output as \"worth a closer look,\" not a verdict.\n# The only authoritative fix is to upgrade.\n#\n# Usage: sudo bash nginx-rift-check.sh\n# Exit: 0 = nothing flagged, 1 = something flagged, 2 = couldn't run\n\nset -u\n\n# ---------- gather config ----------\ntmp=$(mktemp) || { echo \"mktemp failed\" &gt;&amp;2; exit 2; }\ntrap 'rm -f \"$tmp\"' EXIT\n\nver_raw=\"\"\nis_plus=0\nplus_rev=\"\"\n\nif command -v nginx &gt;/dev/null 2&gt;&amp;1; then\n    ver_raw=$(nginx -v 2&gt;&amp;1)\n    if ! nginx -T 2&gt;/dev/null &gt; \"$tmp\"; then\n        find /etc/nginx /usr/local/nginx/conf /opt/nginx/conf 2&gt;/dev/null \\\n            -type f \\( -name \"*.conf\" -o -path \"*/sites-enabled/*\" -o -path \"*/conf.d/*\" \\) \\\n            -print0 2&gt;/dev/null | xargs -0 -r cat &gt; \"$tmp\" 2&gt;/dev/null\n    fi\nelse\n    find /etc/nginx /usr/local/nginx/conf /opt/nginx/conf 2&gt;/dev/null \\\n        -type f \\( -name \"*.conf\" -o -path \"*/sites-enabled/*\" -o -path \"*/conf.d/*\" \\) \\\n        -print0 2&gt;/dev/null | xargs -0 -r cat &gt; \"$tmp\" 2&gt;/dev/null\nfi\n\nif [ ! -s \"$tmp\" ]; then\n    echo \"ERROR: no nginx config found and 'nginx -T' produced nothing.\" &gt;&amp;2\n    echo \"If nginx is under a non-standard prefix, edit the find paths.\" &gt;&amp;2\n    exit 2\nfi\n\n# ---------- version check ----------\noss_ver=$(printf '%s' \"$ver_raw\" | sed -nE 's#.*nginx/([0-9]+\\.[0-9]+\\.[0-9]+).*#\\1#p')\nif printf '%s' \"$ver_raw\" | grep -qi 'nginx-plus'; then\n    is_plus=1\n    plus_rev=$(printf '%s' \"$ver_raw\" | sed -nE 's#.*nginx-plus-(r[0-9]+(-p[0-9]+)?).*#\\1#ip')\nfi\n\nver_verdict=\"UNKNOWN\"\nif [ \"$is_plus\" -eq 1 ]; then\n    rnum=$(printf '%s' \"$plus_rev\" | sed -nE 's#r([0-9]+).*#\\1#ip')\n    if [ -n \"$rnum\" ] &amp;&amp; [ \"$rnum\" -ge 32 ] &amp;&amp; [ \"$rnum\" -le 36 ]; then\n        ver_verdict=\"AFFECTED RANGE (NGINX Plus $plus_rev \u2014 check P-level against advisory)\"\n    else\n        ver_verdict=\"not in known affected Plus range\"\n    fi\nelif [ -n \"$oss_ver\" ]; then\n    n=$(printf '%s' \"$oss_ver\" | awk -F. '{print $1*1000000+$2*1000+$3}')\n    if [ \"$n\" -ge 6027 ] &amp;&amp; [ \"$n\" -le 1030000 ]; then\n        ver_verdict=\"AFFECTED RANGE (OSS $oss_ver)\"\n    else\n        ver_verdict=\"not in known affected OSS range (OSS $oss_ver)\"\n    fi\nfi\n\necho \"nginx version: ${oss_ver:-unknown}${is_plus:+ (Plus $plus_rev)} =&gt; $ver_verdict\"\n\n# ---------- config pattern check ----------\n# Character-by-character tokenizer that emits one statement per { } ;\n# along with the current block_id. Each new \"{\" gets a fresh block_id,\n# so sibling blocks at the same depth are distinct.\nawk '\nBEGIN { RS = \"\\0\" }   # read whole file as one record\n{\n    src = $0\n    L = length(src)\n\n    next_block_id = 1\n    depth = 0\n    stack[0] = 0      # implicit top-level block has id 0\n    buf = \"\"\n    risk = 0\n    flagged = 0\n    in_sq = 0; in_dq = 0\n    in_comment = 0\n\n    for (i = 1; i &lt;= L; i++) {\n        c = substr(src, i, 1)\n\n        # Handle comments (outside quotes only)\n        if (in_comment) {\n            if (c == \"\\n\") in_comment = 0\n            continue\n        }\n\n        # Handle backslash escape (keep both chars in buf, so regex matches work)\n        if (c == \"\\\\\" &amp;&amp; i &lt; L) {\n            buf = buf c substr(src, i+1, 1)\n            i++\n            continue\n        }\n\n        # Toggle quote state\n        if (c == \"\\\"\" &amp;&amp; !in_sq) { in_dq = !in_dq; buf = buf c; continue }\n        if (c == \"'\\''\" &amp;&amp; !in_dq) { in_sq = !in_sq; buf = buf c; continue }\n\n        # Inside a quoted string: copy verbatim, no structural chars\n        if (in_sq || in_dq) { buf = buf c; continue }\n\n        # Start of a comment\n        if (c == \"#\") { in_comment = 1; continue }\n\n        # Structural characters\n        if (c == \"{\") {\n            check_stmt(buf, stack[depth])\n            buf = \"\"\n            depth++\n            stack[depth] = next_block_id++\n            continue\n        }\n        if (c == \"}\") {\n            check_stmt(buf, stack[depth])\n            buf = \"\"\n            delete seen[stack[depth]]\n            if (depth &gt; 0) depth--\n            continue\n        }\n        if (c == \";\") {\n            check_stmt(buf, stack[depth])\n            buf = \"\"\n            continue\n        }\n\n        buf = buf c\n    }\n    check_stmt(buf, stack[depth])\n\n    if (flagged) {\n        print \"config pattern: POSSIBLY VULNERABLE\"\n        exit 0\n    } else if (risk) {\n        print \"config pattern: rewrite with $N + ? found, but no follow-up in same scope\"\n        exit 1\n    } else {\n        print \"config pattern: not found by heuristic\"\n        exit 1\n    }\n}\n\nfunction check_stmt(stmt, blk) {\n    gsub(/[ \\t\\r\\n]+/, \" \", stmt)\n    sub(/^ /, \"\", stmt)\n    sub(/ $/, \"\", stmt)\n    if (stmt == \"\") return\n\n    # Vulnerable rewrite?\n    # - directive is \"rewrite\"\n    # - contains at least one unnamed capture: \"(\" not followed by \"?\", not escaped\n    # - replacement contains \"?\"\n    # - uses a numbered backreference $1..$9\n    if (stmt ~ /^rewrite[ \\t]/ &amp;&amp; \\\n        stmt ~ /\\?/ &amp;&amp; \\\n        stmt ~ /(^|[^\\\\])\\([^?]/ &amp;&amp; \\\n        stmt ~ /\\$[0-9]/) {\n        seen[blk] = 1\n        printf \"  [!] vulnerable rewrite (block_id=%d):\\n      %s\\n\", blk, stmt\n        risk = 1\n        return\n    }\n\n    # Follow-up directive in the same block as a prior vulnerable rewrite\n    if ((blk in seen) &amp;&amp; stmt ~ /^(rewrite|if|set)[ \\t]/) {\n        printf \"  [!] follow-up directive in same scope (block_id=%d):\\n      %s\\n\", blk, stmt\n        flagged = 1\n    }\n}\n' \"$tmp\"\npattern_status=$?\n\n# ---------- verdict ----------\necho \"\"\nif [ \"$pattern_status\" -eq 0 ] &amp;&amp; [[ \"$ver_verdict\" == AFFECTED* ]]; then\n    echo \"RESULT: HIGH RISK \u2014 vulnerable version AND matching config pattern.\"\n    echo \"        Upgrade nginx (OSS &gt;= 1.30.1 / 1.31.0, Plus R32 P6 / R36 P4)\"\n    echo \"        OR replace unnamed captures (\\$1, \\$2) with named captures\"\n    echo \"        (?...) in the flagged rewrite directives.\"\n    exit 1\nelif [ \"$pattern_status\" -eq 0 ]; then\n    echo \"RESULT: config pattern matches but version not flagged.\"\n    echo \"        Double-check the F5 advisory if you're on Plus or a derivative:\"\n    echo \"        https://my.f5.com/manage/s/article/K000161019\"\n    exit 1\nelif [[ \"$ver_verdict\" == AFFECTED* ]]; then\n    echo \"RESULT: vulnerable version but no matching config pattern found.\"\n    echo \"        Still recommended: upgrade. The heuristic can miss things.\"\n    exit 1\nelse\n    echo \"RESULT: not flagged by this heuristic. Upgrading is still recommended.\"\n    exit 0\nfi", "creation_timestamp": "2026-05-16T00:21:10.000000Z"}