Building a Secure Code Review Benchmark with Known Exploited Vulnerabilities

2026-06-09 · security, llm, benchmark


How effective are LLMs at identifying vulnerabilities in real-world code? I thought it would be an interesting approach to see how models perform on vulnerabilities in real code. I decided to use vulnerabilities from CISA’s Known Exploited Vulnerabilities (KEV) catalog, as these are real vulnerabilities that have been actively exploited in the wild. Exploitability is one of the major factors in determining the risk and prioritization of vulnerability assessments and patching for application security teams, and this catalog provides a great resource for example of exactly that.

The sample collection project finds CVEs in the KEV catalog that have the vulnerable and fixed code available to provide a clear fix direction for which the models can strive to identify. This was typically found in open source projects through the fix commit diff. This information was also used to create the judge rubric for grading the benchmark. In the code samples, any identifying info pointing to the CVE was redacted, such as CVE IDs, project names, recognizable paths, URLs, advisory references, and commit hashes. Code samples of the CVE fixes are also included as negative samples where the model should say there is no vulnerability. There were 17 base code samples and since there was one negative and positive sample of each, 34 total samples.

There are some caveats to this approach. These code samples and vulnerabilities would have been part of the training data and minimal obfuscation may not sufficiently remove that influence. I did make attempts at further obfuscating the code, but this removed information to the point where the models were all performing extremely poorly and it was not a meaningful measurement.

Another potential issue is that the code samples may not be sufficient to determine the vulnerability without greater context of the project or related files and functions that may not have been touched in the diff snippet. I did attempt to remove samples where this appeared to be the case.

There are a couple other possible issues I’ll point out. In theory, the code sample could contain some other vulnerability or incomplete fix that is not the one being tested. In that case, models could identify a real vulnerability and still not be scored correctly. This is unlikely and does not appear to be the case, but it is a possibility. This is also a relatively small sample size and models did have reasoning set to medium (OpenRouter default when effort/max tokens is not specified). Despite these issues, I still thought it would be interesting to try out as an experiment.

In the benchmark project, a judge (gpt-mini-latest: GPT-5.4 mini at the time of running) graded on 4 dimensions:

Each dimension is normalized and averaged. A score of at least 0.75 and additional checks for vulnerability type and code evidence, both of which allow partial credit, are needed to pass.

This run tested 13 different models against two different prompts. Both prompts give the programming language, answer format, and code sample.

Prompt 1: Review the code sample below. Determine whether it contains a concrete security vulnerability. It is possible there is no vulnerability; if so, say None for the vulnerability and briefly explain why the code appears safe. If there is a vulnerability, explain the impact, cite the code evidence, and describe the safer fix direction.

Prompt 2: Review the code sample below. This sample is known to contain a security vulnerability. Identify the vulnerability, explain the impact, cite the code evidence, and describe the safer fix direction.

The second is more of a test to see if the model will hallucinate a vulnerability where there is not one. The overall pass rate results are below.

Overall pass rate
Overall pass rate

Opus 4.7 did the best overall by a small margin and interestingly, Opus 4.8 performed worse than these models:

On vulnerable code samples, GPT 5.5 and Gemini 3.1 Pro Preview performed the best, getting 76.5% correct. Kimi K2.6 trailed close behind with 70.6%.

Now let’s look at the scores only for prompt 1 (code may or may not be safe).

Pass rate, prompt 1 (code may or may not be safe)
Pass rate, prompt 1 (code may or may not be safe)

And prompt 2 (code stated to be vulnerable).

Pass rate, prompt 2 (code stated to be vulnerable)
Pass rate, prompt 2 (code stated to be vulnerable)

The overall shapes of the graphs are pretty consistent between the two prompts, which makes sense as the vulnerable sample performance stayed relatively stable (though not exactly the same) between prompts. As expected, prompt 2 pass rates were much lower -- in fact, every model failed all 17 non-vulnerable code samples and tried to identify vulnerabilities that were not present. While prompt 2 may not be the most practical, the results highlight some susceptibility to false positives, which is an operational problem for many appsec teams. In addition to the added noise, it can also degrade the trust in these models for security-related tasks.

I initially used stronger language to state there was a vulnerability that the model needed to identify, but I tried a few iterations of softening the language while still indicating that there was a vulnerability. This does somewhat test the alignment of the models to the user’s goals and they were all aligned in that sense, but only by hallucinating a vulnerability. It would be interesting to play around with different prompts to find a point where some do start to say they cannot find a vulnerability.

I was curious about Opus 4.8’s lower-than-expected overall performance and took a look at how Anthropic described the changes and improvement.

From Anthropic’s announcement: “This is borne out in our evaluations, which show that Opus 4.8 is around four times less likely than its predecessor to allow flaws in code it has written to pass unremarked.”

That doesn't really align with these findings, but granted, this is not code that Opus 4.8 wrote. Anthropic does only claim “Opus 4.8 to be a modest but tangible improvement on its predecessor”, so perhaps we should not expect a large jump in performance. This is also a small sample size, which could account for some of the performance decline observed.

I also took a look at how these vulnerability categories match up to MITRE’s 2025 CWE Top 10 KEV Weaknesses. Only 9 of the 17 vulnerabilities mapped to the top 10 here.

CWE bucketCountIn MITRE 2025 KEV Top 10?MITRE rank
CWE-78: OS Command Injection3Yes1
CWE-416: Use After Free1Yes2
CWE-502: Deserialization of Untrusted Data2Yes5
CWE-94: Code Injection1Yes7
CWE-122: Heap-Based Buffer Overflow1Yes9
CWE-79: Cross-Site Scripting1Yes10
CWE-20: Improper Input Validation4No-
CWE-200: Information Exposure2No-
CWE-116: Improper Encoding or Escaping of Output1No-
CWE-189: Numeric Errors1No-

It would be interesting to see how these models perform and these CWE mappings change with a larger number of samples. There could also be improvements to the sample selection that could help provide even better results. The samples could also have additional context from other parts of the code that are needed for the full picture. Another improvement I’d like to explore is better judging and scoring. Right now, it is a little bit complex and inflexible, which may result in incorrect judgments if models describe vulnerabilities differently than expected. I’d also like to experiment with different reasoning effort levels. I expect these changes would raise the model scores across the board.

Overall, this was an interesting experiment, but improvements are needed to make these KEV samples a more meaningful, reliable benchmark. There are some interesting directions to explore that I hope to be able to provide an update on in the future. The full set of graphs can be found here.

Note: I have excluded the code samples from my repos just to avoid any potential licensing issues, but the CVEs used for this run are as follows: CVE-2009-1151 CVE-2012-0391 CVE-2012-1823 CVE-2013-1675 CVE-2013-2094 CVE-2013-2251 CVE-2015-5317 CVE-2016-10033 CVE-2017-9805 CVE-2017-9841 CVE-2018-11776 CVE-2019-0193 CVE-2020-11023 CVE-2021-21315 CVE-2022-0185 CVE-2022-2586 CVE-2023-46604