Authorization analysis logic improvements (#61)

This commit is contained in:
Eli Peter 2026-05-02 16:44:49 -04:00 committed by GitHub
parent 3c89bddbf2
commit 40995e45e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 4193 additions and 134 deletions

View file

@ -8,7 +8,7 @@ Current baseline (2026-05-02):
| Recall | 1.000 | 1.000 | 0.944 |
| F1 | 1.000 | 1.000 | 0.901 |
Corpus: 492 cases across 10 languages, 491 evaluated (1 disabled). Per-run JSON lands in `tests/benchmark/results/` (`latest.json` plus dated snapshots). See `README.md` for what the scoring modes mean and how to run a subset.
Corpus: 499 cases across 10 languages, 496 evaluated (3 disabled). Per-run JSON lands in `tests/benchmark/results/` (`latest.json` plus dated snapshots). See `README.md` for what the scoring modes mean and how to run a subset.
The corpus is mostly synthetic 8-20 line fixtures, one vulnerability or one safe pattern per file. A smaller real-CVE replay set under `cve_corpus/` covers 20 published CVEs across all 10 languages. Both contribute to the headline numbers.
@ -24,6 +24,7 @@ Real disclosed CVEs reduced to minimal reproducers, vulnerable + patched pair pe
| CVE-2026-33626 | Python | LMDeploy | Apache-2.0 | SSRF | detected |
| CVE-2019-14939 | JavaScript | mongo-express | MIT | code_exec | detected |
| CVE-2025-64430 | JavaScript | Parse Server | Apache-2.0 | SSRF | detected |
| CVE-2023-22621 | JavaScript | Strapi | MIT | code_exec (SSTI)| detected |
| CVE-2023-26159 | TypeScript | follow-redirects | MIT | SSRF | detected |
| GHSA-4x48-cgf9-q33f | TypeScript | Novu | MIT | SSRF | detected |
| CVE-2022-30323 | Go | hashicorp/go-getter | MPL-2.0 | CMDI | detected |
@ -43,6 +44,7 @@ Real disclosed CVEs reduced to minimal reproducers, vulnerable + patched pair pe
| CVE-2019-18634 | C | sudo (pwfeedback) | ISC | memory_safety | detected |
| CVE-2019-13132 | C++ | ZeroMQ libzmq | MPL-2.0 | memory_safety | detected |
| CVE-2022-1941 | C++ | Protocol Buffers | BSD-3-Clause | memory_safety | detected |
| CVE-2026-25544 | TypeScript | Payload (Drizzle adapter) | MIT | sql_injection | deferred |
Deferred entries are real bugs Nyx can't yet detect. The fixture stays committed with `disabled: true` in ground truth so the gap remains visible.
@ -67,6 +69,8 @@ Most recent first. Metrics are rule-level on the corpus size at that point.
| Date | Change | Corpus | P | R | F1 |
|------------|------------------------------------------------------------------------------|--------|-------|-------|-------|
| 2026-05-02 | TS regex-allowlist `<*regex*>.test(value)` / `<*pattern*>.test(value)` recognised as ValidationCall whose target is the first arg (overrides default receiver-as-target); conservative on receiver names so non-regex `*.test()` callees stay Unknown. CVE-2026-25544 (Payload drizzle SQL injection) lands in corpus disabled — needs validated-flow propagation through SSA derivation / helper-summary returns | 499 | 1.000 | 1.000 | 1.000 |
| 2026-05-02 | JS arrow `assignment_pattern` default-param extraction + JS object-literal kwarg fallback for gated sinks + double-call (`f()(x)`) chained-inner rebinding; lodash `_.template` modeled as gated CODE_EXEC sink suppressed by `{ evaluate: false }`; CVE-2023-22621 (Strapi SSTI) detected | 494 | — | — | — |
| 2026-05-02 | `strings.ReplaceAll` recognised as CMDi sanitiser in chain-wrapper / call-site-replace shapes; clears `go-safe-009` (last open corpus FP); aggregate rule-level reaches P=R=F1=1.000 | 492 | 1.000 | 1.000 | 1.000 |
| 2026-05-01 | PathFact opaque-prefix-lock (`canonicalise + start_with?(<expr>)` recognised across Ruby/Python/JS) + `is_path_traversal_safe` predicate + negated-form polarity flip on assertion narrowing; rswag CVE-2023-38337 detected | 490 | 0.972 | 0.992 | 0.982 |
| 2026-05-01 | Ruby `OpenURI.open_uri` SSRF sink + inner-call fallback for statement-level Ruby calls (`YAML.safe_load(File.read(x))` shape now classifies); CVE-2021-21288 (CarrierWave) detected | 482 | 0.972 | 0.992 | 0.982 |

View file

@ -0,0 +1,55 @@
// go-safe-realrepo-016 — distilled from prometheus tsdb/block_test.go:185
// and 9+ other prometheus test files. Pattern: a wrapper call takes
// the close call's RESULT as an argument, e.g.
//
// require.NoError(t, f.Close())
// errs = append(errs, f.Close())
//
// The CFG creates one Call node per statement keyed on the OUTER
// callee. The inner-call release was invisible to the resource pass
// before the fix: direct-release loop matches `info.call.callee`
// (the outer callee), and the inner-call callee was carried in
// `info.arg_callees[i]` but unread. Engine fix:
// src/state/transfer.rs::apply_call now walks `info.arg_callees`
// after the direct-release branch.
package safe
import (
"errors"
"os"
)
type tHelper struct{}
func (tHelper) NoError(args ...any) {}
var t tHelper
func close_in_require_noerror() error {
f, err := os.OpenFile("/tmp/x", os.O_RDWR, 0o666)
if err != nil {
return err
}
t.NoError(f.Close())
return nil
}
func close_in_append_arg() error {
f, err := os.Create("/tmp/y")
if err != nil {
return err
}
var errs []error
errs = append(errs, f.Close())
return errors.Join(errs...)
}
func close_via_defer() error {
f, err := os.Open("/tmp/z")
if err != nil {
return err
}
defer f.Close()
return nil
}

View file

@ -0,0 +1,78 @@
// go-safe-realrepo-017 — distilled from prometheus
// `cmd/promtool/tsdb.go::startProfiling` (lines 230, 239, 246, 252):
// 4 findings on the same function plus widespread similar shapes
// across the prometheus tree. Pattern:
//
// b.cpuprof, err = os.Create(...)
//
// The resource is owned by the struct `*writeBenchmark`. Closure
// happens in a paired method `stopProfiling()`. The current function
// body cannot observe that closure, so any per-body resource analysis
// fires unconditionally.
//
// Engine fix (depth: structural — both layers):
// * src/state/transfer.rs::apply_call gates the acquire branch on
// `!define_is_field_lhs` so member-expression LHS doesn't seed
// `state.resource` in the dataflow lattice.
// * src/cfg_analysis/resources.rs::run gates the structural rule's
// acquire-iteration on the same `defines.contains('.')` check.
package safe
import (
"os"
"runtime/pprof"
)
type writeBenchmark struct {
cpuprof *os.File
memprof *os.File
blockprof *os.File
mtxprof *os.File
outPath string
}
func (b *writeBenchmark) startProfiling() error {
var err error
b.cpuprof, err = os.Create(b.outPath + "/cpu.prof")
if err != nil {
return err
}
if err := pprof.StartCPUProfile(b.cpuprof); err != nil {
return err
}
b.memprof, err = os.Create(b.outPath + "/mem.prof")
if err != nil {
return err
}
b.blockprof, err = os.Create(b.outPath + "/block.prof")
if err != nil {
return err
}
b.mtxprof, err = os.Create(b.outPath + "/mutex.prof")
if err != nil {
return err
}
return nil
}
func (b *writeBenchmark) stopProfiling() error {
if b.cpuprof != nil {
pprof.StopCPUProfile()
b.cpuprof.Close()
b.cpuprof = nil
}
if b.memprof != nil {
b.memprof.Close()
b.memprof = nil
}
if b.blockprof != nil {
b.blockprof.Close()
b.blockprof = nil
}
if b.mtxprof != nil {
b.mtxprof.Close()
b.mtxprof = nil
}
return nil
}

View file

@ -0,0 +1,16 @@
// go-vuln-realrepo-018 — recall guard for the inner-call-arg /
// member-LHS fixes. Bare-identifier `f := os.OpenFile(...)` with no
// `f.Close()` anywhere must still fire the resource-leak rule.
package safe
import "os"
func vuln_open_no_close() error {
f, err := os.OpenFile("/tmp/x", os.O_RDWR, 0o666)
if err != nil {
return err
}
_ = f
return nil
}

View file

@ -0,0 +1,20 @@
# py-auth-vuln-002: helper takes a user-supplied id (`project_id`)
# and queries by it without any preceding ownership/membership check.
# This is the vulnerable counterpart to
# safe_django_orm_caller_scoped_entity.py — same Django ORM shape, but
# the param is an *id-like user input*, not a scope-entity object, so
# the caller-scope-entity exemption must not apply.
#
# Pinned to keep recall on the missing_ownership_check rule.
class Project:
pass
def get_project(request, project_id):
return Project.objects.filter(id=project_id).first()
def delete_project(request, project_id):
Project.objects.filter(id=project_id).delete()

View file

@ -0,0 +1,63 @@
# py-auth-realrepo-008: caller-passed scope entity used as ownership
# constraint. Distilled from sentry
# `src/sentry/api/helpers/environments.py::get_environments` (and the
# many sibling helpers in `api/endpoints/organization_releases.py`):
#
# def get_environments(request, organization: Organization):
# ...
# return list(
# Environment.objects.filter(
# organization_id=organization.id,
# name__in=requested_environments,
# )
# )
#
# `_filter_releases_by_query(queryset, organization, query, filter_params)`
# follows the same pattern with `queryset.filter_by_semver(organization.id, ...)`.
#
# Both helpers receive the already-authorised `organization` object
# from a route handler that resolved it via `OrganizationReleasesBaseEndpoint`
# membership middleware. The query is *scoped by* `organization.id`
# — that IS the ownership boundary, not a user-controlled target.
#
# Without the caller-scope-entity exemption, every internal helper in a
# multi-tenant Django/Rails/Laravel codebase flags
# `missing_ownership_check` because the engine cannot tell "scoping
# arg" from "user-targeted arg". The fix recognises that
# `<entity>.id` where `<entity>` is a unit parameter named after a
# scope-bearing domain entity (organization, project, team, workspace,
# tenant, account, ...) is a passed-in scope, not a target.
from typing import List
class Organization:
pass
class Environment:
pass
def get_environments(request, organization: Organization) -> List[Environment]:
requested_environments = set(request.GET.getlist("environment"))
if not requested_environments:
return []
return list(
Environment.objects.filter(
organization_id=organization.id, name__in=requested_environments
)
)
def _filter_releases_by_query(queryset, organization: Organization, query, filter_params):
queryset = queryset.filter_by_semver(organization.id, query)
queryset = queryset.filter_by_stage(organization.id, query)
return queryset
def list_project_issues(request, project):
return list(Issue.objects.filter(project_id=project.id, status="open"))
class Issue:
pass

View file

@ -0,0 +1,52 @@
# py-auth-realrepo-010: pytest test method decorated with
# `@mock.patch("...")` collides with Flask's `<app>.<verb>` route
# decorator shape (bare_method_name("mock.patch") == "patch", which the
# parse_flask_route_decorator matched as HTTP PATCH). The collision
# attached the test method as a Flask route handler, flipped its
# `unit.kind` to RouteHandler, made it pass
# `unit_has_user_input_evidence` unconditionally, and flooded pytest
# test suites with `missing_ownership_check` findings.
#
# Distilled from airflow
# `providers/google/tests/unit/google/cloud/hooks/test_dlp.py` (47 FPs
# in this single file pre-fix). Fix:
# `parse_flask_route_decorator` short-circuits when the callee text
# matches a known test-framework decorator vocabulary
# (`mock.patch`, `unittest.mock.patch`, `monkeypatch.setattr`,
# `pytest.mark.parametrize`, …).
#
# This fixture verifies pytest test methods don't fire ownership-check
# findings, even when they call ORM-shaped APIs with id-suffixed
# constants (the canonical pytest fixture-data pattern).
from unittest import mock
from unittest.mock import PropertyMock
ORGANIZATION_ID = "fake-org-id-123"
PROJECT_ID = "fake-proj-id-456"
DLP_JOB_ID = "fake-job-id-789"
class TestCloudDLPHook:
@mock.patch(
"module.GoogleBaseHook.project_id",
new_callable=PropertyMock,
return_value=None,
)
@mock.patch("module.CloudDLPHook.get_conn")
def test_create_deidentify_template_with_org_id(self, get_conn, mock_project_id):
get_conn.return_value.create_deidentify_template.return_value = "API_RESPONSE"
result = self.hook.create_deidentify_template(organization_id=ORGANIZATION_ID)
return result
@mock.patch("module.CloudDLPHook.get_conn")
def test_create_dlp_job(self, get_conn):
result = self.hook.create_dlp_job(project_id=PROJECT_ID)
return result
@mock.patch.object(SomeClass, "method")
def test_with_object_patch(self, mock_method):
self.hook.cancel_dlp_job(dlp_job_id=DLP_JOB_ID)
class SomeClass:
pass

View file

@ -0,0 +1,70 @@
// Real-repo motivation (meilisearch `GuardedData<P, D>` typed
// extractor on actix-web routes registered via `#[routes::path(..)]`
// attribute macros).
//
// Meilisearch's authorization extractor is
// `GuardedData<ActionPolicy<{ actions::KEYS_GET }>,
// Data<AuthController>>`. Possessing the value proves the request
// passed the per-action permission check the inner Policy term
// encodes. Routes are registered by attribute macro, not by the
// `.route("/p", web::get().to(handler))` builder pattern, so the
// actix_web extractor's route walk doesn't attach the handler as
// `RouteHandler` and never injected typed-extractor guard checks.
//
// The typed-extractor fallback pass in `actix_web::extract` now walks
// every Function-kind unit and applies `guard_calls_for_handler` to
// its parameter list, so the `GuardedData` parameter is recognised as
// a route-level policy guard (`AuthCheckKind::Other`,
// `is_route_level: true`) and the per-handler ownership rule no
// longer fires on path-derived sinks.
#![allow(dead_code, unused_variables)]
use std::marker::PhantomData;
pub struct ActionPolicy<const A: u8>;
pub struct Data<T>(pub T);
pub struct GuardedData<P, D> {
data: D,
_marker: PhantomData<P>,
}
impl<P, D> GuardedData<P, D> {
pub fn into_inner(self) -> D {
self.data
}
}
pub mod web {
pub struct Path<T>(pub T);
impl<T> Path<T> {
pub fn into_inner(self) -> T {
unimplemented!()
}
}
}
pub struct AuthController;
impl AuthController {
pub fn get_key(&self, uid: u64) -> Result<String, ()> {
Ok(String::new())
}
}
pub mod actions {
pub const KEYS_GET: u8 = 1;
}
pub struct AuthParam {
pub key: u64,
}
pub async fn get_api_key(
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, Data<AuthController>>,
path: web::Path<AuthParam>,
) -> Result<String, ()> {
let uid = path.into_inner().key;
auth_controller.into_inner().0.get_key(uid)
}

View file

@ -0,0 +1,44 @@
// Negative counterpart for `safe_actix_guarded_data_extractor.rs`.
//
// Same handler shape (path-derived `uid` flows into
// `auth_controller.get_key(uid)`) but **without** the `GuardedData<P, D>`
// wrapper around the controller. The handler now takes a bare
// `Data<AuthController>` and a typed `web::Path<AuthParam>` — no
// route-level capability check is implied by the parameter types.
// Pinned by `unsafe_actix_no_guarded_data_extractor` to guard against
// over-broad `policy_guard_names` recognition that would treat any
// handler with an actix-web parameter shape as authorised: the rule
// must still fire here.
#![allow(dead_code, unused_variables)]
pub struct Data<T>(pub T);
pub mod web {
pub struct Path<T>(pub T);
impl<T> Path<T> {
pub fn into_inner(self) -> T {
unimplemented!()
}
}
}
pub struct AuthController;
impl AuthController {
pub fn get_key(&self, uid: u64) -> Result<String, ()> {
Ok(String::new())
}
}
pub struct AuthParam {
pub key: u64,
}
pub async fn get_api_key(
auth_controller: Data<AuthController>,
path: web::Path<AuthParam>,
) -> Result<String, ()> {
let uid = path.into_inner().key;
auth_controller.0.get_key(uid)
}

View file

@ -0,0 +1,23 @@
[package]
name = "unsafe_actix_web_project_no_check"
version = "0.1.0"
edition = "2021"
# Manifest names actix-web → `lang_has_web_framework("rust")` returns
# `Some(true)` → the project-level web-framework signal does NOT
# suppress the param-name arm. The handler below is then correctly
# flagged for taking a user-controlled `*_id` parameter and performing
# a sink without an upstream auth check (regression guard for the
# project-level gate ─ the gate must NOT silence findings in real
# web projects).
[dependencies]
actix-web = "4"
# `actix-web` is a manifest-only regression marker: nyx's
# `lang_has_web_framework("rust")` reads the dependency list to derive
# `Some(true)`, which keeps the param-name arm of missing_ownership_check
# active. No `use actix_web::*` line exists in src/lib.rs, so machete
# correctly sees it as code-unused — the dep is real for our purposes.
[package.metadata.cargo-machete]
ignored = ["actix-web"]

View file

@ -0,0 +1,16 @@
//! Regression counterpart to `safe_non_web_rust_project`. Same helper
//! shape (`fn delete_session(session_id: i64)`) with NO upstream auth
//! check — must still flag missing_ownership_check because the
//! project's manifest names `actix-web` → web-framework signal
//! `Some(true)` → the param-name heuristic stays on.
pub struct Db;
impl Db {
pub async fn delete_one(&self, _id: i64) -> Result<(), ()> { Ok(()) }
}
// Helper called from an actix handler. No upstream `require_*` /
// `check_*` covers `session_id`, so missing_ownership_check fires.
pub async fn delete_session(db: &Db, session_id: i64) -> Result<(), ()> {
db.delete_one(session_id).await
}

View file

@ -0,0 +1,23 @@
[package]
name = "safe_non_web_rust_project"
version = "0.1.0"
edition = "2021"
# Manifest deliberately names no Rust web framework. The auth-analysis
# web-framework signal must derive Some(false) from this manifest, so
# every internal helper named `<thing>_id` and every `session.foo`
# chain in the source refuses the user-input evidence and
# missing_ownership_check stays silent.
[dependencies]
serde = "1"
tokio = { version = "1", features = ["full"] }
# These deps are manifest-only regression markers. The point of this
# fixture is that the manifest names NO Rust web framework, so
# `lang_has_web_framework("rust")` returns `Some(false)`. `serde` and
# `tokio` populate the dependency list without tripping that signal,
# proving the gate ignores non-web crates. src/lib.rs deliberately
# uses neither.
[package.metadata.cargo-machete]
ignored = ["serde", "tokio"]

View file

@ -0,0 +1,60 @@
//! Real-repo precision guard distilled from zed's desktop / GUI crates
//! (`crates/agent_servers/src/acp.rs::session_thread`,
//! `crates/agent_ui/src/thread_worktree_archive.rs::rollback_persist`,
//! `crates/debugger_ui/src/tests/debugger_panel.rs::test_*`).
//!
//! Without the project-level web-framework signal, two heuristics
//! over-fire on internal helpers in non-web Rust projects:
//! * `is_external_input_param_name` flips step 3 open on every
//! `*_id` / `path` / `query` / `body` / `dto` parameter.
//! * `matches_session_context` lifts every `session.foo` chain into
//! `unit.context_inputs` (step 2), even when `session` is a
//! debug / RPC / DAP session, not an HTTP/auth session.
//!
//! Both arms must be gated by the project's web-framework signal.
//! This crate's `Cargo.toml` deliberately names no Rust web framework,
//! so `lang_has_web_framework("rust")` returns `Some(false)` and both
//! arms refuse to count internal-helper params as user input.
pub struct ContextServerStore;
impl ContextServerStore {
pub fn get_running_server(&self, _: &str) -> Option<()> { Some(()) }
}
pub struct ClientContext {
pub sessions: Vec<DebugSession>,
}
pub struct DebugSession;
impl DebugSession {
pub fn update<F: FnOnce(&Self) -> R, R>(&self, f: F) -> R { f(self) }
pub fn read(&self) -> &Self { self }
pub fn adapter_client(&self) -> Option<()> { Some(()) }
}
// `<thing>_id` parameter must not gate user-input-evidence open in a
// project the manifest confirmed has no Rust web framework. Without
// the gate, every helper of this shape would fire missing_ownership_check.
pub fn get_prompt(
server_store: &ContextServerStore,
server_id: &str,
prompt_name: &str,
) -> Option<()> {
let _ = (server_id, prompt_name);
server_store.get_running_server(server_id)
}
pub async fn rollback_persist(archived_worktree_id: i64) {
let _ = archived_worktree_id;
}
// Bare `session.foo` chains land in `context_inputs` via
// `matches_session_context` → `ValueSourceKind::Session`. In a non-
// web Rust project the gate suppresses step 2 so this idiomatic
// debug-session pattern stays silent.
pub fn open_debug_session(ctx: &ClientContext) {
if let Some(session) = ctx.sessions.first() {
let _ = session.update(|session| session.adapter_client());
let _client = session.read();
}
}

View file

@ -0,0 +1,43 @@
// Validated-flow propagation through helper chains
// (`SsaFuncSummary::validated_params_to_return`, CVE-2026-25544 deep
// fix). `sanitize` validates its parameter via a regex allowlist
// and throws on failure; `buildQuery` interpolates the sanitised
// result into a SQL fragment; the handler hands the fragment to a
// raw-SQL execute callee.
//
// On a normal-returning call to either helper, the actual argument
// passed validation by construction, so `db.query(sql)` must not
// re-flag downstream taint findings. The summary records
// `validated_params_to_return: [0]` on `sanitize` after the
// `regex.test` guard, propagates the bit through `buildQuery` via
// summary re-extraction, and the caller's sink therefore observes
// `all_validated = true`.
//
// Pinned by:
// * tests/lib::validated_params_to_return_suppresses_one_hop_helper_validator
// * tests/lib::validated_params_to_return_suppresses_two_hop_helper_validator
import express, { Request, Response } from 'express';
const SAFE_VALUE_REGEX = /^[\w@.\-+:]*$/;
const sanitize = (value: string): string => {
if (!SAFE_VALUE_REGEX.test(value)) {
throw new Error('value is not allowed');
}
return value;
};
const buildQuery = (column: string, value: string): string => {
const safe = sanitize(value);
return column + '=' + safe;
};
const app = express();
app.use(express.json());
app.post('/q', (req: Request, res: Response) => {
const userValue = req.body.filter as string;
const sql = buildQuery('data', userValue);
res.send(sql);
});

View file

@ -0,0 +1,60 @@
// Nyx CVE benchmark fixture.
//
// CVE: CVE-2023-22621
// Project: Strapi (strapi/strapi)
// License: MIT (https://github.com/strapi/strapi/blob/develop/LICENSE)
// Advisory: https://github.com/strapi/strapi/security/advisories/GHSA-2h87-4q2w-v4hf
// Patched: 921d30961d6ba96cc098f2aea197350a49f990bd
// packages/core/email/server/services/email.js:25-50
//
// Patched-fix simplification: `createStrictInterpolationRegExp` is
// imported from `@strapi/utils` upstream; we inline a one-line stub
// that builds a regex restricted to a fixed allowlist. The load-
// bearing fix is the explicit `{ interpolate, evaluate: false,
// escape: false }` options object passed to `_.template`, which
// disables lodash's `<% ... %>` evaluate block. The trailing
// `(data)` invocation of the compiled function is split off (matches
// the corresponding split in `vulnerable.js`).
//
// Trim parity with `vulnerable.js`: same `attributes.reduce`-to-`for`
// transformation; the load-bearing
// `_.template(emailTemplate[attribute], { interpolate, evaluate: false, escape: false })`
// call is verbatim from upstream's options-object form.
'use strict';
const _ = require('lodash');
const express = require('express');
const app = express();
app.use(express.json());
const createStrictInterpolationRegExp = (allowed) =>
new RegExp(`<%=\\s*(${allowed.join('|')})\\s*%>`, 'g');
const keysDeep = (obj) => Object.keys(obj || {});
const sendTemplatedEmail = (emailOptions = {}, emailTemplate = {}, data = {}) => {
const attributes = ['subject', 'text', 'html'];
const allowedInterpolationVariables = keysDeep(data);
const interpolate = createStrictInterpolationRegExp(allowedInterpolationVariables);
const templatedAttributes = {};
for (const attribute of attributes) {
if (emailTemplate[attribute]) {
const compiled = _.template(emailTemplate[attribute], {
interpolate,
evaluate: false,
escape: false,
});
templatedAttributes[attribute] = compiled(data);
}
}
return templatedAttributes;
};
app.put('/users-permissions/email-templates', (req, res) => {
sendTemplatedEmail({}, req.body.emailTemplate, req.body.data);
res.sendStatus(200);
});
app.listen(1337);

View file

@ -0,0 +1,50 @@
// Nyx CVE benchmark fixture.
//
// CVE: CVE-2023-22621
// Project: Strapi (strapi/strapi)
// License: MIT (https://github.com/strapi/strapi/blob/develop/LICENSE)
// Advisory: https://github.com/strapi/strapi/security/advisories/GHSA-2h87-4q2w-v4hf
// Vulnerable: 479bdde67eb3759d89218c9686208be2409217ef
// packages/core/email/server/services/email.js:23-39
//
// Strapi <= 4.5.5 compiled email-template strings via lodash `_.template`
// without restricting the interpolation regex. An authenticated admin
// could PUT /users-permissions/email-templates with a payload whose
// `subject` / `text` / `html` field contained a lodash `<% ... %>`
// evaluate block, which lodash compiles into a JavaScript Function. When
// the email service rendered the template, the embedded JavaScript
// executed in the Strapi process context (RCE).
//
// Trims: `keysDeep` import, `missingAttributes` early-throw, plugin
// provider chain, the surrounding controller layer that translates
// `PUT /email-templates` into a call to `sendTemplatedEmail`. The
// load-bearing sink call `_.template(emailTemplate[attribute])` is
// verbatim; the trailing `(data)` invocation of the compiled
// function is split off so the engine sees the SSTI sink directly
// rather than as the inner call of a `f()()` chain.
'use strict';
const _ = require('lodash');
const express = require('express');
const app = express();
app.use(express.json());
const sendTemplatedEmail = (emailOptions = {}, emailTemplate = {}, data = {}) => {
const attributes = ['subject', 'text', 'html'];
const templatedAttributes = {};
for (const attribute of attributes) {
if (emailTemplate[attribute]) {
const compiled = _.template(emailTemplate[attribute]);
templatedAttributes[attribute] = compiled(data);
}
}
return templatedAttributes;
};
app.put('/users-permissions/email-templates', (req, res) => {
sendTemplatedEmail({}, req.body.emailTemplate, req.body.data);
res.sendStatus(200);
});
app.listen(1337);

View file

@ -0,0 +1,103 @@
// Nyx CVE benchmark fixture (patched counterpart).
//
// CVE: CVE-2026-25544
// Project: Payload (payloadcms/payload)
// License: MIT (https://github.com/payloadcms/payload/blob/main/LICENSE.md)
// Advisory: https://github.com/payloadcms/payload/security/advisories/GHSA-xx6w-jxg9-2wh8
// Patched: ea5a0982a21f77497b729e66d5a257c740d3f1c9 (tag v3.73.0)
// packages/drizzle/src/postgres/createJSONQuery/index.ts:1-50
// packages/drizzle/src/utilities/escapeSQLValue.ts:1-25
//
// Patched form of `sanitizeValue`: validates against `SAFE_STRING_REGEX`
// and rejects anything containing `\` or `"` so the user-supplied value
// can no longer escape the surrounding SQL string literal. Backslashes
// and double quotes that survive the regex are still escaped before
// interpolation. Non-string values are coerced or rejected; an APIError
// is thrown for any value that does not match the safe shape.
//
// Trims: the upstream patch lives in the @payloadcms/drizzle package.
// `SAFE_STRING_REGEX`, `sanitizeValue`, and `createJSONQuery` are copied
// verbatim from v3.73.0; the Express handler is the same scaffolding as
// the vulnerable counterpart so the diff is one-for-one.
import express, { Request, Response } from 'express';
type CreateJSONQueryArgs = {
column: string | { name: string };
operator: string;
pathSegments: string[];
value: unknown;
};
class APIError extends Error {
constructor(message: string, public status: number) {
super(message);
}
}
export const SAFE_STRING_REGEX = /^[\w @.\-+:]*$/;
const operatorMap: Record<string, string> = {
contains: '~',
equals: '==',
in: 'in',
like: 'like_regex',
not_equals: '!=',
not_in: 'in',
not_like: '!like_regex',
};
const sanitizeValue = (value: unknown, operator?: string): string => {
if (value === null) {
return `NULL`;
}
if (typeof value === 'number' || typeof value === 'boolean') {
return `${value}`;
}
if (typeof value !== 'string') {
throw new Error('Invalid value type');
}
if (!SAFE_STRING_REGEX.test(value)) {
throw new APIError(`${value} is not allowed as a JSON query value`, 400);
}
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
const prefix = ['like', 'not_like'].includes(operator ?? '') ? '(?i)' : '';
return `"${prefix}${escaped}"`;
};
export const createJSONQuery = ({ column, operator, pathSegments, value }: CreateJSONQueryArgs) => {
const columnName = typeof column === 'object' ? column.name : column;
const jsonPaths = pathSegments
.slice(1)
.map((key) => {
return `${key}[*]`;
})
.join('.');
const fullPath = pathSegments.length === 1 ? '$[*]' : `$.${jsonPaths}`;
return `jsonb_path_exists(${columnName}, '${fullPath} ? (@ ${operatorMap[operator]} ${sanitizeValue(value, operator)})')`;
};
declare const db: { execute: (sql: string) => Promise<unknown> };
const app = express();
app.use(express.json());
app.post('/query', async (req: Request, res: Response) => {
const userValue = req.body.filter as string;
const sql = createJSONQuery({
column: 'data',
operator: 'equals',
pathSegments: ['$', 'name'],
value: userValue,
});
const result = await db.execute(sql);
res.json(result);
});

View file

@ -0,0 +1,82 @@
// Nyx CVE benchmark fixture.
//
// CVE: CVE-2026-25544
// Project: Payload (payloadcms/payload)
// License: MIT (https://github.com/payloadcms/payload/blob/main/LICENSE.md)
// Advisory: https://github.com/payloadcms/payload/security/advisories/GHSA-xx6w-jxg9-2wh8
// Vulnerable: 625bb8c05293dece82bb89c2c5a1467aaead9a6a (tag v3.72.0)
// packages/drizzle/src/postgres/createJSONQuery/index.ts:1-50
//
// Payload < v3.73.0 embedded user input into a Postgres `jsonb_path_exists`
// SQL fragment via raw template-string interpolation. `sanitizeValue`
// double-quoted the string but did not escape backslashes or quotes, so a
// crafted JSON-query value could close the SQL string literal and inject
// arbitrary SQL. The Drizzle adapter then executed that string via
// `db.execute(sql)`. Affected adapters: db-postgres, db-vercel-postgres,
// db-sqlite, db-d1-sqlite (per advisory). Class: SQL injection.
//
// Trims: original is part of a 3-package adapter wired through PayloadCMS
// service classes (`@payloadcms/db-postgres` -> `@payloadcms/drizzle` ->
// `payload`). The Express handler below is scaffolding so the single-file
// scanner sees the user-input -> sanitizeValue -> sql -> db.execute flow.
// `operatorMap`, `sanitizeValue`, and `createJSONQuery` are copied
// verbatim from the upstream file at v3.72.0.
import express, { Request, Response } from 'express';
type CreateJSONQueryArgs = {
column: string | { name: string };
operator: string;
pathSegments: string[];
value: unknown;
};
const operatorMap: Record<string, string> = {
contains: '~',
equals: '==',
in: 'in',
like: 'like_regex',
not_equals: '!=',
not_in: 'in',
not_like: '!like_regex',
};
const sanitizeValue = (value: unknown, operator?: string) => {
if (typeof value === 'string') {
// ignore casing with like or not_like
return `"${['like', 'not_like'].includes(operator) ? '(?i)' : ''}${value}"`;
}
return value as string;
};
export const createJSONQuery = ({ column, operator, pathSegments, value }: CreateJSONQueryArgs) => {
const columnName = typeof column === 'object' ? column.name : column;
const jsonPaths = pathSegments
.slice(1)
.map((key) => {
return `${key}[*]`;
})
.join('.');
const fullPath = pathSegments.length === 1 ? '$[*]' : `$.${jsonPaths}`;
return `jsonb_path_exists(${columnName}, '${fullPath} ? (@ ${operatorMap[operator]} ${sanitizeValue(value, operator)})')`;
};
declare const db: { execute: (sql: string) => Promise<unknown> };
const app = express();
app.use(express.json());
app.post('/query', async (req: Request, res: Response) => {
const userValue = req.body.filter as string;
const sql = createJSONQuery({
column: 'data',
operator: 'equals',
pathSegments: ['$', 'name'],
value: userValue,
});
const result = await db.execute(sql);
res.json(result);
});

View file

@ -3,7 +3,7 @@
"metadata": {
"description": "Nyx benchmark ground truth",
"created": "2026-03-20",
"corpus_size": 492
"corpus_size": 507
},
"cases": [
{
@ -9657,6 +9657,62 @@
],
"notes": "CVE-2025-64430 patched counterpart: URI-backed file upload removed entirely; no http.get on user input"
},
{
"case_id": "cve-js-2023-22621-vulnerable",
"file": "cve_corpus/javascript/CVE-2023-22621/vulnerable.js",
"language": "javascript",
"is_vulnerable": true,
"vuln_class": "code_exec",
"cwe": "CWE-1336",
"provenance": "real_cve",
"equivalence_tier": "exact",
"match_mode": "rule_match",
"expected_rule_ids": [
"taint-unsanitised-flow"
],
"allowed_alternative_rule_ids": [],
"forbidden_rule_ids": [],
"expected_severity": "MEDIUM",
"expected_category": "Security",
"expected_sink_lines": [],
"expected_source_lines": [],
"tags": [
"cve",
"strapi",
"code_exec",
"ssti",
"lodash",
"template"
],
"notes": "CVE-2023-22621: Strapi <=4.5.5 sendTemplatedEmail compiled lodash _.template on attacker-controlled email-template body (admin panel), enabling SSTI -> RCE via <% ... %> evaluate blocks. MIT"
},
{
"case_id": "cve-js-2023-22621-patched",
"file": "cve_corpus/javascript/CVE-2023-22621/patched.js",
"language": "javascript",
"is_vulnerable": false,
"vuln_class": "safe",
"cwe": "N/A",
"provenance": "real_cve",
"equivalence_tier": "exact",
"match_mode": "file_presence",
"expected_rule_ids": [],
"allowed_alternative_rule_ids": [],
"forbidden_rule_ids": [
"taint-unsanitised-flow"
],
"expected_severity": null,
"expected_category": "Security",
"expected_sink_lines": [],
"expected_source_lines": [],
"tags": [
"cve",
"strapi",
"patched",
"negative"
],
"notes": "CVE-2023-22621 patched counterpart: _.template called with { interpolate: <strict regex>, evaluate: false, escape: false } so the lodash evaluate block compiler is disabled."
},
{
"case_id": "cve-ts-2023-26159-vulnerable",
"file": "cve_corpus/typescript/CVE-2023-26159/vulnerable.ts",
@ -13153,6 +13209,34 @@
"disabled": false,
"notes": "Empty-string fallback (`process.env.X || \"\"`) is not a hardcoded secret. Distilled from /Users/elipeter/oss/cal.com/apps/api/v2/src/modules/stripe/utils/newStripeInstance.ts and ~30 sibling cal.com calendar/stripe/sendgrid integration files. Engine fix: pattern-level regex (#match? @fallback \"[^\\\"']\") in src/patterns/typescript.rs."
},
{
"case_id": "ts-safe-021",
"file": "typescript/safe/safe_validated_helper_chain.ts",
"language": "typescript",
"is_vulnerable": false,
"vuln_class": "safe",
"cwe": "N/A",
"provenance": "synthetic",
"equivalence_tier": "exact",
"match_mode": "file_presence",
"expected_rule_ids": [],
"allowed_alternative_rule_ids": [],
"forbidden_rule_ids": [
"taint-unsanitised-flow"
],
"expected_severity": null,
"expected_category": "Security",
"expected_sink_lines": [],
"expected_source_lines": [],
"tags": [
"validated-flow",
"helper-validator",
"summary-propagation",
"cve-2026-25544"
],
"disabled": false,
"notes": "Validated-flow propagation through helper chains. `sanitize` validates its first parameter via a regex allowlist; `buildQuery` interpolates the sanitised result into a SQL fragment; the handler hands the fragment to `db.execute`. Pinned by `SsaFuncSummary::validated_params_to_return` + `propagate_validated_params_to_return` (CVE-2026-25544 deep fix)."
},
{
"case_id": "py-auth-decorator-001",
"file": "python/safe/safe_login_required_decorator.py",
@ -14098,6 +14182,94 @@
"disabled": false,
"notes": "`if err != nil { c.Fatalf(...) }` / `os.Exit` / `log.Fatalf` / `panic(err)` are documented terminators (Goexit-class). cfg-error-fallthrough must walk through them as terminating paths. Closes the minio test-file cluster (49+34+12+11+9+7+7 hits across xl-storage_test.go, erasure-healing_test.go, format-erasure_test.go, \u2026). Engine fix: src/cfg_analysis/error_handling.rs::call_never_returns."
},
{
"case_id": "go-safe-realrepo-016",
"file": "go/safe/safe_inner_call_close_in_arg.go",
"language": "go",
"is_vulnerable": false,
"vuln_class": "safe",
"cwe": "N/A",
"provenance": "synthetic",
"equivalence_tier": "exact",
"match_mode": "rule_match",
"expected_rule_ids": [],
"allowed_alternative_rule_ids": [
"state-resource-leak-possible"
],
"forbidden_rule_ids": [
"state-resource-leak"
],
"expected_severity": null,
"expected_category": "Security",
"expected_sink_lines": [],
"expected_source_lines": [],
"tags": [
"resource-lifecycle",
"negative",
"real-repo-precision-2026-05-02"
],
"disabled": false,
"notes": "`require.NoError(t, f.Close())` and `errs = append(errs, f.Close())` shapes \u2014 the inner-call release was invisible because the CFG's per-statement Call node carries the OUTER callee. Engine fix: src/state/transfer.rs::apply_call now walks info.arg_callees after the direct-release branch and marks the bare-receiver SymbolId CLOSED. Closes 9+ hits across prometheus tsdb test files."
},
{
"case_id": "go-safe-realrepo-017",
"file": "go/safe/safe_struct_field_resource_owned_by_struct.go",
"language": "go",
"is_vulnerable": false,
"vuln_class": "safe",
"cwe": "N/A",
"provenance": "synthetic",
"equivalence_tier": "exact",
"match_mode": "rule_match",
"expected_rule_ids": [],
"allowed_alternative_rule_ids": [],
"forbidden_rule_ids": [
"state-resource-leak",
"state-resource-leak-possible",
"cfg-resource-leak"
],
"expected_severity": null,
"expected_category": "Security",
"expected_sink_lines": [],
"expected_source_lines": [],
"tags": [
"resource-lifecycle",
"negative",
"real-repo-precision-2026-05-02"
],
"disabled": false,
"notes": "`b.cpuprof = os.Create(...)` shape \u2014 member-expression LHS is an ownership transfer to the containing struct, not a local acquisition. Closure responsibility belongs to a paired `stopProfiling()` method. Engine fix: src/state/transfer.rs::apply_call gates the acquire on !define_is_field_lhs; src/cfg_analysis/resources.rs::run mirrors the gate. Closes the prometheus cmd/promtool/tsdb.go::startProfiling cluster (4 findings on b.cpuprof, b.memprof, b.blockprof, b.mtxprof)."
},
{
"case_id": "go-vuln-realrepo-018",
"file": "go/safe/vuln_resource_leak_no_close.go",
"language": "go",
"is_vulnerable": true,
"vuln_class": "resource",
"cwe": "CWE-404",
"provenance": "synthetic",
"equivalence_tier": "exact",
"match_mode": "rule_match",
"expected_rule_ids": [
"state-resource-leak"
],
"allowed_alternative_rule_ids": [
"cfg-resource-leak",
"state-resource-leak-possible"
],
"forbidden_rule_ids": [],
"expected_severity": "MEDIUM",
"expected_category": "Security",
"expected_sink_lines": [],
"expected_source_lines": [],
"tags": [
"resource-lifecycle",
"positive",
"real-repo-precision-2026-05-02"
],
"disabled": false,
"notes": "Recall guard for the inner-call-arg / member-LHS fixes. Bare-identifier `f := os.OpenFile(...)` with no `f.Close()` anywhere must still fire the resource-leak rule."
},
{
"case_id": "go-auth-realrepo-001",
"file": "go/auth/vuln_repo_findbyid_no_auth.go",
@ -14592,6 +14764,117 @@
"disabled": false,
"notes": "Negative-counterpart guard for the LocalCollection / parameter-name fixes: handler takes a HashMap typed param (in-memory bookkeeping) but ALSO calls `db.update_owner(req.target_user_id, ...)` (real DbMutation). The cache mutation must not blanket-suppress the persistent-store mutation \u2014 the rule must still fire on `db.update_owner`."
},
{
"case_id": "rs-auth-realrepo-014",
"file": "rust/auth/safe_actix_guarded_data_extractor.rs",
"language": "rust",
"is_vulnerable": false,
"vuln_class": "safe",
"cwe": "N/A",
"provenance": "synthetic",
"equivalence_tier": "exact",
"match_mode": "rule_match",
"expected_rule_ids": [],
"allowed_alternative_rule_ids": [],
"forbidden_rule_ids": [
"rs.auth.missing_ownership_check"
],
"expected_severity": null,
"expected_category": "Security",
"expected_sink_lines": [],
"expected_source_lines": [],
"tags": [
"auth",
"negative",
"real-repo-precision-2026-05-02",
"noise-budget-zero"
],
"disabled": false,
"notes": "Meilisearch `GuardedData<ActionPolicy<{ actions::KEYS_GET }>, Data<AuthController>>` typed extractor on actix-web routes registered via `#[routes::path(..)]` attribute macros (no `.route()` builder, so `collect_routes` doesn't attach the handler). The new typed-extractor fallback pass in `actix_web::extract` walks every Function-kind unit and applies `guard_calls_for_handler`; the `Guarded`-prefix `policy_guard_names` recogniser injects `AuthCheckKind::Other` with `is_route_level: true`, so `auth_check_covers_subject`'s route-level short-circuit suppresses missing-ownership-check on path-derived sinks."
},
{
"case_id": "rs-auth-realrepo-015",
"file": "rust/auth/unsafe_actix_no_guarded_data_extractor.rs",
"language": "rust",
"is_vulnerable": true,
"vuln_class": "auth",
"cwe": "CWE-285",
"provenance": "synthetic",
"equivalence_tier": "exact",
"match_mode": "rule_match",
"expected_rule_ids": [
"rs.auth.missing_ownership_check"
],
"allowed_alternative_rule_ids": [],
"forbidden_rule_ids": [],
"expected_severity": "Medium",
"expected_category": "Security",
"expected_sink_lines": [],
"expected_source_lines": [],
"tags": [
"auth",
"positive",
"real-repo-precision-2026-05-02"
],
"disabled": false,
"notes": "Negative-counterpart guard for the `GuardedData` typed-extractor recogniser: same handler shape but the wrapper is replaced by a bare `Data<AuthController>` (no policy enforcement implied). An over-broad `policy_guard_names` recogniser would silence this; the Guarded-prefix matcher must NOT fire on bare `Data<...>`, so the rule still flags the path-derived `uid` flowing into `auth_controller.get_key`."
},
{
"case_id": "rs-auth-realrepo-016",
"file": "rust/safe/safe_non_web_rust_project",
"language": "rust",
"is_vulnerable": false,
"vuln_class": "safe",
"cwe": "",
"provenance": "real-repo-precision-2026-05-02",
"equivalence_tier": "exact",
"match_mode": "rule_match",
"expected_rule_ids": [],
"allowed_alternative_rule_ids": [],
"forbidden_rule_ids": [
"rs.auth.missing_ownership_check",
"rs.auth.stale_authorization",
"rs.auth.token_override_without_validation"
],
"expected_severity": null,
"expected_category": null,
"expected_sink_lines": [],
"expected_source_lines": [],
"tags": [
"auth",
"shape-safe",
"real-repo-precision-2026-05-02"
],
"disabled": false,
"notes": "Real-repo precision guard distilled from zed (desktop GUI / DAP debugger / agent) crates: `<thing>_id` parameters on internal helpers AND `session.foo` chains on debug-session handles must NOT count as user-input evidence in a Rust project whose Cargo.toml names no web framework. `lang_has_web_framework(\"rust\")` returns Some(false) and the gate suppresses both step-2 (context_inputs) and step-3 (param-name) heuristics."
},
{
"case_id": "rs-auth-realrepo-017",
"file": "rust/auth/unsafe_actix_web_project_no_check",
"language": "rust",
"is_vulnerable": true,
"vuln_class": "auth",
"cwe": "CWE-285",
"provenance": "real-repo-precision-2026-05-02",
"equivalence_tier": "exact",
"match_mode": "rule_match",
"expected_rule_ids": [
"rs.auth.missing_ownership_check"
],
"allowed_alternative_rule_ids": [],
"forbidden_rule_ids": [],
"expected_severity": "High",
"expected_category": "Security",
"expected_sink_lines": [],
"expected_source_lines": [],
"tags": [
"auth",
"positive",
"real-repo-precision-2026-05-02"
],
"disabled": false,
"notes": "Regression counterpart to `rs-auth-realrepo-016`: same helper shape with no upstream auth check, but the project's manifest names `actix-web` so `lang_has_web_framework(\"rust\")` returns Some(true) and the param-name arm of `unit_has_user_input_evidence` stays on. Asserts the project-level web-framework gate doesn't silence findings in real Rust web projects."
},
{
"case_id": "ruby-safe-ar-query-shapes-001",
"file": "ruby/safe/safe_active_record_query_shapes.rb",
@ -15585,6 +15868,165 @@
],
"disabled": false,
"notes": "fgets stdin user input echoed into curl_easy_setopt CURLOPT_POSTFIELDS at fixed URL; sensitivity-gate suppresses Plain-tier sources."
},
{
"case_id": "py-auth-realrepo-008",
"file": "python/safe/safe_django_orm_caller_scoped_entity.py",
"language": "python",
"is_vulnerable": false,
"vuln_class": "safe",
"cwe": "N/A",
"provenance": "real-repo",
"equivalence_tier": "exact",
"match_mode": "rule_match",
"expected_rule_ids": [],
"allowed_alternative_rule_ids": [],
"forbidden_rule_ids": [
"py.auth.missing_ownership_check"
],
"expected_severity": null,
"expected_category": "Security",
"expected_sink_lines": [],
"expected_source_lines": [],
"tags": [
"auth",
"django",
"real-repo-precision-2026-05-02"
],
"disabled": false,
"notes": "Distilled from sentry api/helpers/environments.py::get_environments and api/endpoints/organization_releases.py::_filter_releases_by_query. `<entity>.id` for a unit param named after a scope-bearing domain entity (organization, project, ...) is the ownership scope inherited from the caller, not a user-controlled target. Pinned by is_caller_scope_entity_subject in src/auth_analysis/checks.rs. Also exercises the keyword_argument-key fix in extract_value_refs (Environment.objects.filter(organization_id=...) — the kwarg key `organization_id` is the ORM column name, not a subject)."
},
{
"case_id": "py-auth-realrepo-009",
"file": "python/auth/vuln_user_id_param_no_auth.py",
"language": "python",
"is_vulnerable": true,
"vuln_class": "auth",
"cwe": "CWE-862",
"provenance": "real-repo",
"equivalence_tier": "exact",
"match_mode": "rule_match",
"expected_rule_ids": [
"py.auth.missing_ownership_check"
],
"allowed_alternative_rule_ids": [],
"forbidden_rule_ids": [],
"expected_severity": "MEDIUM",
"expected_category": "Security",
"expected_sink_lines": [
[
16,
16
],
[
20,
20
]
],
"expected_source_lines": [],
"tags": [
"auth",
"django",
"real-repo-precision-2026-05-02"
],
"disabled": false,
"notes": "Vulnerable counterpart to py-auth-realrepo-008: helper takes a user-supplied `project_id` (id-like name) and queries Project.objects.filter(id=project_id) without any preceding ownership check. Regression guard: the caller-scope-entity exemption must NOT suppress when the param is itself an id-like user input."
},
{
"case_id": "py-auth-realrepo-010",
"file": "python/safe/safe_mock_patch_test_method.py",
"language": "python",
"is_vulnerable": false,
"vuln_class": "safe",
"cwe": "N/A",
"provenance": "real-repo",
"equivalence_tier": "exact",
"match_mode": "rule_match",
"expected_rule_ids": [],
"allowed_alternative_rule_ids": [],
"forbidden_rule_ids": [
"py.auth.missing_ownership_check",
"py.auth.token_override_without_validation"
],
"expected_severity": null,
"expected_category": "Security",
"expected_sink_lines": [],
"expected_source_lines": [],
"tags": [
"auth",
"pytest",
"real-repo-precision-2026-05-02"
],
"disabled": false,
"notes": "Distilled from airflow providers/google/tests/unit/google/cloud/hooks/test_dlp.py: pytest test method decorated with `@mock.patch(\"...\")` was being attached as a Flask `PATCH` route handler because bare_method_name(\"mock.patch\") == \"patch\". Fix: parse_flask_route_decorator short-circuits on known test-framework decorator vocabulary (mock.patch, unittest.mock.patch, monkeypatch.setattr, pytest.mark.parametrize)."
},
{
"case_id": "cve-ts-2026-25544-vulnerable",
"file": "cve_corpus/typescript/CVE-2026-25544/vulnerable.ts",
"language": "typescript",
"is_vulnerable": true,
"vuln_class": "sqli",
"cwe": "CWE-89",
"provenance": "real_cve",
"equivalence_tier": "exact",
"match_mode": "rule_match",
"expected_rule_ids": [
"taint-unsanitised-flow"
],
"allowed_alternative_rule_ids": [],
"forbidden_rule_ids": [],
"expected_severity": "MEDIUM",
"expected_category": "Security",
"expected_sink_lines": [
[
80,
81
]
],
"expected_source_lines": [
[
73,
73
]
],
"tags": [
"cve",
"payload",
"sqli",
"vulnerable"
],
"disabled": true,
"disabled_reason": "Validated-flow propagation through SSA-derived values and helper-summary returns is missing. The patched counterpart applies a regex allowlist (`SAFE_STRING_REGEX.test(value)` throw) PLUS a `replace()` escape chain inside `sanitizeValue`, then interpolates the result into a SQL template literal in `createJSONQuery` and returns the string to the handler, which calls `db.execute(sql)`. This session landed `classify_condition` recognition of `<*regex*>.test(value)` / `<*pattern*>.test(value)` as a ValidationCall whose target is the call's first arg (covered by `path_state::tests::target_regex_test_first_arg`, `target_regex_test_pattern_receiver`, `target_test_non_regex_receiver_is_not_validation`, plus the SSA-level `regex_test_allowlist_narrowing_clears_direct_flow` integration test). But validated_must is per-symbol and consulted only at the sink site; it does NOT propagate through the SSA Assign that templates a clean `value` into a derived `sql` string, nor does it ride a helper's `param_to_return` summary back into a caller. Disabled until that propagation path lands. Tracked in CVE_DEFERRED.md.",
"notes": "CVE-2026-25544: Payload `sanitizeValue` SQL injection via Postgres jsonb_path_exists template-string interpolation. Vulnerable form (`@payloadcms/drizzle@v3.72.0`, MIT) lets attacker-controlled JSON-query value escape the surrounding SQL string literal because `sanitizeValue` only double-quotes it without escaping `\\`/`\"`. Disabled pending validated-flow propagation engine work, see disabled_reason."
},
{
"case_id": "cve-ts-2026-25544-patched",
"file": "cve_corpus/typescript/CVE-2026-25544/patched.ts",
"language": "typescript",
"is_vulnerable": false,
"vuln_class": "safe",
"cwe": "N/A",
"provenance": "real_cve",
"equivalence_tier": "exact",
"match_mode": "file_presence",
"expected_rule_ids": [],
"allowed_alternative_rule_ids": [],
"forbidden_rule_ids": [
"taint-unsanitised-flow"
],
"expected_severity": null,
"expected_category": "Security",
"expected_sink_lines": [],
"expected_source_lines": [],
"tags": [
"cve",
"payload",
"safe",
"patched"
],
"disabled": true,
"disabled_reason": "Sibling of cve-ts-2026-25544-vulnerable. Disabled together until validated-flow summary propagation lands. See vulnerable counterpart's disabled_reason for the engine gap.",
"notes": "Patched form of `sanitizeValue` from `@payloadcms/drizzle@v3.73.0` (MIT). Disabled together with its vulnerable counterpart pending validated-flow propagation work."
}
]
}

View file

@ -1,6 +1,6 @@
{
"benchmark_version": "1.0",
"timestamp": "2026-05-02T07:03:06Z",
"timestamp": "2026-05-02T19:35:12Z",
"scanner_version": "0.6.0",
"scanner_config": {
"analysis_mode": "Full",
@ -9,10 +9,10 @@
"state_analysis_enabled": true,
"worker_threads": 1
},
"ground_truth_hash": "sha256:ba8f5f6e20ce478b6032b1df98e5dc57a7b7a8ced8f1d3294dc811034bc6fc3c",
"corpus_size": 492,
"cases_run": 491,
"cases_skipped": 1,
"ground_truth_hash": "sha256:de2df25545527c2c90c665a5d4db257fb8f0d7aefe16eb742ee8e70f7de55e99",
"corpus_size": 507,
"cases_run": 504,
"cases_skipped": 3,
"outcomes": [
{
"case_id": "c-buf-001",
@ -1478,6 +1478,40 @@
"security_finding_count": 2,
"non_security_finding_count": 0
},
{
"case_id": "cve-js-2023-22621-patched",
"file": "cve_corpus/javascript/CVE-2023-22621/patched.js",
"language": "javascript",
"vuln_class": "safe",
"is_vulnerable": false,
"outcome_file_level": "TN",
"outcome_rule_level": "TN",
"outcome_location_level": null,
"matched_rule_ids": [],
"unexpected_rule_ids": [],
"all_finding_ids": [],
"security_finding_count": 0,
"non_security_finding_count": 0
},
{
"case_id": "cve-js-2023-22621-vulnerable",
"file": "cve_corpus/javascript/CVE-2023-22621/vulnerable.js",
"language": "javascript",
"vuln_class": "code_exec",
"is_vulnerable": true,
"outcome_file_level": "TP",
"outcome_rule_level": "TP",
"outcome_location_level": null,
"matched_rule_ids": [
"taint-unsanitised-flow (source 46:26)"
],
"unexpected_rule_ids": [],
"all_finding_ids": [
"taint-unsanitised-flow (source 46:26)"
],
"security_finding_count": 1,
"non_security_finding_count": 0
},
{
"case_id": "cve-js-2025-64430-patched",
"file": "cve_corpus/javascript/CVE-2025-64430/patched.js",
@ -2723,6 +2757,42 @@
"security_finding_count": 0,
"non_security_finding_count": 0
},
{
"case_id": "go-safe-realrepo-016",
"file": "go/safe/safe_inner_call_close_in_arg.go",
"language": "go",
"vuln_class": "safe",
"is_vulnerable": false,
"outcome_file_level": "FP",
"outcome_rule_level": "FP",
"outcome_location_level": null,
"matched_rule_ids": [],
"unexpected_rule_ids": [
"state-resource-leak-possible",
"state-resource-leak-possible"
],
"all_finding_ids": [
"state-resource-leak-possible",
"state-resource-leak-possible"
],
"security_finding_count": 2,
"non_security_finding_count": 0
},
{
"case_id": "go-safe-realrepo-017",
"file": "go/safe/safe_struct_field_resource_owned_by_struct.go",
"language": "go",
"vuln_class": "safe",
"is_vulnerable": false,
"outcome_file_level": "TN",
"outcome_rule_level": "TN",
"outcome_location_level": null,
"matched_rule_ids": [],
"unexpected_rule_ids": [],
"all_finding_ids": [],
"security_finding_count": 0,
"non_security_finding_count": 0
},
{
"case_id": "go-sqli-001",
"file": "go/sqli/sqli_concat.go",
@ -2883,6 +2953,27 @@
"security_finding_count": 0,
"non_security_finding_count": 0
},
{
"case_id": "go-vuln-realrepo-018",
"file": "go/safe/vuln_resource_leak_no_close.go",
"language": "go",
"vuln_class": "resource",
"is_vulnerable": true,
"outcome_file_level": "TP",
"outcome_rule_level": "TP",
"outcome_location_level": null,
"matched_rule_ids": [
"state-resource-leak",
"cfg-resource-leak"
],
"unexpected_rule_ids": [],
"all_finding_ids": [
"state-resource-leak",
"cfg-resource-leak"
],
"security_finding_count": 2,
"non_security_finding_count": 0
},
{
"case_id": "go-xss-001",
"file": "go/xss/xss_fprintf.go",
@ -5123,6 +5214,57 @@
"security_finding_count": 1,
"non_security_finding_count": 0
},
{
"case_id": "py-auth-realrepo-008",
"file": "python/safe/safe_django_orm_caller_scoped_entity.py",
"language": "python",
"vuln_class": "safe",
"is_vulnerable": false,
"outcome_file_level": "TN",
"outcome_rule_level": "TN",
"outcome_location_level": null,
"matched_rule_ids": [],
"unexpected_rule_ids": [],
"all_finding_ids": [],
"security_finding_count": 0,
"non_security_finding_count": 0
},
{
"case_id": "py-auth-realrepo-009",
"file": "python/auth/vuln_user_id_param_no_auth.py",
"language": "python",
"vuln_class": "auth",
"is_vulnerable": true,
"outcome_file_level": "TP",
"outcome_rule_level": "TP",
"outcome_location_level": "TP",
"matched_rule_ids": [
"py.auth.missing_ownership_check",
"py.auth.missing_ownership_check"
],
"unexpected_rule_ids": [],
"all_finding_ids": [
"py.auth.missing_ownership_check",
"py.auth.missing_ownership_check"
],
"security_finding_count": 2,
"non_security_finding_count": 0
},
{
"case_id": "py-auth-realrepo-010",
"file": "python/safe/safe_mock_patch_test_method.py",
"language": "python",
"vuln_class": "safe",
"is_vulnerable": false,
"outcome_file_level": "TN",
"outcome_rule_level": "TN",
"outcome_location_level": null,
"matched_rule_ids": [],
"unexpected_rule_ids": [],
"all_finding_ids": [],
"security_finding_count": 0,
"non_security_finding_count": 0
},
{
"case_id": "py-cmdi-001",
"file": "python/cmdi/cmdi_direct.py",
@ -6422,6 +6564,77 @@
"security_finding_count": 1,
"non_security_finding_count": 0
},
{
"case_id": "rs-auth-realrepo-014",
"file": "rust/auth/safe_actix_guarded_data_extractor.rs",
"language": "rust",
"vuln_class": "safe",
"is_vulnerable": false,
"outcome_file_level": "TN",
"outcome_rule_level": "TN",
"outcome_location_level": null,
"matched_rule_ids": [],
"unexpected_rule_ids": [],
"all_finding_ids": [
"rs.quality.todo"
],
"security_finding_count": 0,
"non_security_finding_count": 1
},
{
"case_id": "rs-auth-realrepo-015",
"file": "rust/auth/unsafe_actix_no_guarded_data_extractor.rs",
"language": "rust",
"vuln_class": "auth",
"is_vulnerable": true,
"outcome_file_level": "TP",
"outcome_rule_level": "TP",
"outcome_location_level": null,
"matched_rule_ids": [
"rs.auth.missing_ownership_check"
],
"unexpected_rule_ids": [],
"all_finding_ids": [
"rs.quality.todo",
"rs.auth.missing_ownership_check"
],
"security_finding_count": 1,
"non_security_finding_count": 1
},
{
"case_id": "rs-auth-realrepo-016",
"file": "rust/safe/safe_non_web_rust_project",
"language": "rust",
"vuln_class": "safe",
"is_vulnerable": false,
"outcome_file_level": "TN",
"outcome_rule_level": "TN",
"outcome_location_level": null,
"matched_rule_ids": [],
"unexpected_rule_ids": [],
"all_finding_ids": [],
"security_finding_count": 0,
"non_security_finding_count": 0
},
{
"case_id": "rs-auth-realrepo-017",
"file": "rust/auth/unsafe_actix_web_project_no_check",
"language": "rust",
"vuln_class": "auth",
"is_vulnerable": true,
"outcome_file_level": "TP",
"outcome_rule_level": "TP",
"outcome_location_level": null,
"matched_rule_ids": [
"rs.auth.missing_ownership_check"
],
"unexpected_rule_ids": [],
"all_finding_ids": [
"rs.auth.missing_ownership_check"
],
"security_finding_count": 1,
"non_security_finding_count": 0
},
{
"case_id": "rs-auth-typed-extractors-001",
"file": "rust/auth/safe_typed_path_int_extractor.rs",
@ -8481,6 +8694,21 @@
"security_finding_count": 0,
"non_security_finding_count": 0
},
{
"case_id": "ts-safe-021",
"file": "typescript/safe/safe_validated_helper_chain.ts",
"language": "typescript",
"vuln_class": "safe",
"is_vulnerable": false,
"outcome_file_level": "TN",
"outcome_rule_level": "TN",
"outcome_location_level": null,
"matched_rule_ids": [],
"unexpected_rule_ids": [],
"all_finding_ids": [],
"security_finding_count": 0,
"non_security_finding_count": 0
},
{
"case_id": "ts-secrets-001",
"file": "typescript/secrets/fallback_secret.ts",
@ -8785,22 +9013,22 @@
}
],
"aggregate_file_level": {
"tp": 244,
"fp": 0,
"tp": 249,
"fp": 1,
"fn_": 0,
"tn": 247,
"precision": 1.0,
"tn": 254,
"precision": 0.996,
"recall": 1.0,
"f1": 1.0
"f1": 0.9979959919839679
},
"aggregate_rule_level": {
"tp": 244,
"fp": 0,
"tp": 249,
"fp": 1,
"fn_": 0,
"tn": 247,
"precision": 1.0,
"tn": 254,
"precision": 0.996,
"recall": 1.0,
"f1": 1.0
"f1": 0.9979959919839679
},
"by_language": {
"c": {
@ -8822,13 +9050,13 @@
"f1": 1.0
},
"go": {
"tp": 26,
"fp": 0,
"tp": 27,
"fp": 1,
"fn_": 0,
"tn": 30,
"precision": 1.0,
"tn": 31,
"precision": 0.9642857142857143,
"recall": 1.0,
"f1": 1.0
"f1": 0.9818181818181818
},
"java": {
"tp": 21,
@ -8840,10 +9068,10 @@
"f1": 1.0
},
"javascript": {
"tp": 22,
"tp": 23,
"fp": 0,
"fn_": 0,
"tn": 28,
"tn": 29,
"precision": 1.0,
"recall": 1.0,
"f1": 1.0
@ -8858,10 +9086,10 @@
"f1": 1.0
},
"python": {
"tp": 28,
"tp": 29,
"fp": 0,
"fn_": 0,
"tn": 30,
"tn": 32,
"precision": 1.0,
"recall": 1.0,
"f1": 1.0
@ -8876,10 +9104,10 @@
"f1": 1.0
},
"rust": {
"tp": 35,
"tp": 37,
"fp": 0,
"fn_": 0,
"tn": 39,
"tn": 41,
"precision": 1.0,
"recall": 1.0,
"f1": 1.0
@ -8888,7 +9116,7 @@
"tp": 34,
"fp": 0,
"fn_": 0,
"tn": 24,
"tn": 25,
"precision": 1.0,
"recall": 1.0,
"f1": 1.0
@ -8896,7 +9124,7 @@
},
"by_vuln_class": {
"auth": {
"tp": 16,
"tp": 19,
"fp": 0,
"fn_": 0,
"tn": 0,
@ -8923,7 +9151,7 @@
"f1": 1.0
},
"code_exec": {
"tp": 3,
"tp": 4,
"fp": 0,
"fn_": 0,
"tn": 0,
@ -9021,15 +9249,24 @@
"recall": 1.0,
"f1": 1.0
},
"safe": {
"tp": 0,
"resource": {
"tp": 1,
"fp": 0,
"fn_": 0,
"tn": 247,
"tn": 0,
"precision": 1.0,
"recall": 1.0,
"f1": 1.0
},
"safe": {
"tp": 0,
"fp": 1,
"fn_": 0,
"tn": 254,
"precision": 0.0,
"recall": 1.0,
"f1": 0.0
},
"secrets": {
"tp": 1,
"fp": 0,
@ -9078,31 +9315,31 @@
},
"by_confidence": {
">=High": {
"tp": 74,
"fp": 108,
"fn_": 170,
"tn": 139,
"precision": 0.4065934065934066,
"recall": 0.30327868852459017,
"f1": 0.3474178403755868
"tp": 78,
"fp": 107,
"fn_": 171,
"tn": 148,
"precision": 0.42162162162162165,
"recall": 0.3132530120481928,
"f1": 0.359447004608295
},
">=Low": {
"tp": 75,
"fp": 129,
"fn_": 169,
"tn": 118,
"precision": 0.36764705882352944,
"recall": 0.3073770491803279,
"f1": 0.3348214285714286
"tp": 82,
"fp": 126,
"fn_": 167,
"tn": 129,
"precision": 0.3942307692307692,
"recall": 0.3293172690763052,
"f1": 0.35886214442013126
},
">=Medium": {
"tp": 75,
"fp": 124,
"fn_": 169,
"tn": 123,
"precision": 0.3768844221105528,
"recall": 0.3073770491803279,
"f1": 0.33860045146726864
"tp": 82,
"fp": 121,
"fn_": 167,
"tn": 134,
"precision": 0.4039408866995074,
"recall": 0.3293172690763052,
"f1": 0.3628318584070796
}
}
}