Skip to content

Instantly share code, notes, and snippets.

@muhanLuo
Last active May 10, 2026 20:30
Show Gist options
  • Select an option

  • Save muhanLuo/8c888dc2764a55724eeadf035b830e98 to your computer and use it in GitHub Desktop.

Select an option

Save muhanLuo/8c888dc2764a55724eeadf035b830e98 to your computer and use it in GitHub Desktop.
"""
Script Name: wp_find-missing-auth-ajax-vulns.py
Description: Scans a directory of WordPress plugins for AJAX hooks with missing authorization issues. Then triages these findings using Claude. Output format is .sarif
Author: Muhan Luo
Date: 2026-05-10
(Note: This script is specifically intended to be used with the following Semgrep rule, otherwise the script doesn't work: https://github.com/muhanLuo/wordpress-plugin-semgrep-rules/blob/main/missing-auth/wp-ajax-hook-missing-auth.yml)
Dependency Versions:
anthropic==0.100.0
semgrep==1.157.0
Example Usage:
python3 wp_find-missing-auth-ajax-vulns.py --plugin-repo "/home/user/Top 10000 WP Plugins/unzipped/" --rule-location "/home/user/missing-auth/wp-ajax-hook-missing-auth.yml"
"""
import os
import subprocess
import anthropic
import json
import argparse
# Sets up the Claude Client
claude_client = anthropic.Anthropic(
api_key="<API_KEY>",
)
parser = argparse.ArgumentParser()
parser.add_argument('-p', '--plugin-repo', help='Directory containing all the WordPress plugins to be scanned', required=True)
parser.add_argument('-r', '--rule-location', help='Location of the Missing Auth Semgrep rule', required=True)
args = parser.parse_args()
plugin_repo = args.plugin_repo # Directory containing all the WordPress plugins to be scanned
rule_location = args.rule_location # Location of the Missing Auth Semgrep rule
# Loads the cache file. This file stores a list of plugins which have been already scanned and reviewed.
already_scanned_plugins = []
if ".already-scanned" in os.listdir():
cache_file = open(".already-scanned")
already_scanned_plugins = cache_file.read().split("\n")
cache_file.close()
for plugin in os.listdir(plugin_repo):
if plugin in already_scanned_plugins:
print(f"Skipping {plugin}, already scanned...")
else:
print("----------------------")
print(f"Scanning {plugin}...")
sarif_location = f"{plugin}-scan.sarif"
command = [
"semgrep",
"scan",
"--config",
rule_location,
"--sarif",
"--output",
sarif_location,
"--quiet", # Suppress non-essential output
"--timeout",
"360",
plugin_repo + plugin,
]
try:
# Run semgrep to scan the plugin's codebase. Output is a SARIF file
subprocess.run(command, check=True)
print(f"Semgrep analysis completed for {plugin}.")
# Read the SARIF file created by Semgrep and analyze it using Claude
print(f"Starting Claude Analysis for {sarif_location}.")
f = open(sarif_location)
new_sarif_output = json.loads(f.read())
f.close()
results = new_sarif_output["runs"][0]["results"]
# Iterate through each finding in the SARIF file
for finding in range(len(results)):
# Extract the AJAX hook's callback
code_snippet = results[finding]["locations"][0]["physicalLocation"]["region"]["snippet"]["text"]
"""
Send the code snippet to Claude and have Claude review it.
"""
claude_response = claude_client.messages.create(
model="claude-opus-4-6",
max_tokens=20000,
temperature=1,
system=[{"type":"text",
"cache_control": {"type": "ephemeral"},
"text":"You are an expert application security engineer reviewing WordPress PHP code. You will be provided with a PHP function snippet (let's call this $VULN_FUNC) which was found to not include capability checks or nonce verification according to Semgrep. This was determined by looking for functions which did not contain any of these functions in their body: current_user_can(), wp_verify_nonce(), check_ajax_referer(), and check_admin_referer(). \n\nYour goal is to determine if it would be a security risk if $VULN_FUNC could be called by a Subscriber-level user in WordPress. You will return a risk score from 1-5 ( 1 meaning very low security risk and 5 very high security risk).\n\nScore 5: High likelihood + High impact (e.g., arbitrary file deletion, SQL injection)\nScore 4: High likelihood + Medium impact OR Medium likelihood + High impact\nScore 3: Medium likelihood + Medium impact\nScore 2: Low likelihood OR low impact\nScore 1: Minimal risk (e.g., reading non-sensitive public data)\n\nYour methodology for determining the score is as follows:\n\n- First, determine the likelihood of exploitation. We are only looking functions with no-auth and which do not perform nonce checks. If $VULN_FUNC's body contains any function whose name strongly suggests that it performs nonce verification or authorization checks, significantly lower the score. (Ex: check_nonce(), verify_auth(), but obviously not limited to these examples) Important note that the is_admin() function in WordPress does not actually check for authorization. If uncertain, err towards lower scores.\n- Next, determine the impact. If $VULN_FUNC could be called by a Subscriber-level user, would this be a security issue? For example, downgrade the score if $VULN_FUNC doesn't appear to take user input and/or doesn't perform any sensitive or business critical mutating operations. Again, err towards lower scores if uncertain. \n\nYour return format should be a string that follows this format:\n\nscore, explanation\n\n\"score\" should be an integer value 1-5 representing the risk score determined. The second part \"explanation\" should contain a very brief (150 words or less) explanation of how the score was determined."}],
messages=[
{
"role": "user",
"content": [
{
"type": "text",
"text": code_snippet
}
]
}
]
)
output_message = claude_response.content[0].text # Extracts the response message from the Textblock object returned by Claude
new_sarif_output["runs"][0]["results"][finding]["message"]["text"] = output_message # Save the message to the new output file
# Write Claude's analysis to the SARIF file
o = open(sarif_location, "w")
o.write(json.dumps(new_sarif_output))
o.close()
# Write to cache file that we've scanned this plugin's code with Semgrep + analyzed with Claude
cache_file = open(".already-scanned", "a")
cache_file.write(plugin + "\n")
cache_file.close()
print(f"Finished Semgrep + Claude Analysis for {sarif_location}.")
except Exception as e:
print(f"Semgrep + Claude failed for {plugin}")
print(e)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment