feat(learning-harness): absence rows travel with their registration

Eight of the ten vendored access-control rows had no named handler at all,
so every one of them would have scored `missing_context` forever rather
than as a detector miss. After the re-harvest nine are named and the tenth
states its reason.

The handler-context mode now carries the class its framework registers,
multi-line decorators, and commented-out guards -- `#@token_required`
above a ThreatByte handler is the entire bug. Where the registration lives
in another document (connexion's `operationId`, an express router), a
recipe names `context = [...]`, the harvester writes that stanza per side,
and the pair materializes both files at their repository-relative paths so
the entry-point mapper sees the layout the project has. `harvest_recipe`
fetches every blob before the first write and the driver takes `--report`,
so a failed fetch leaves a reviewed excerpt exactly as it was.

Two defects the re-harvest exposed: `.mjs` was not a known suffix, so the
one pair vendored as an ES module had no file target at all; and
`_python_route_access` matched auth words inside decorator prose, reading
`@ns.doc(description='... without proper authorization checks')` as a
guard. Recorded in the hygiene artifact: nine of the ten duplicate groups
sit on both sides of the split, which the repository-level split cannot
see.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
Marius Ciepluch
2026-09-06 12:39:55 +02:00
co-authored by Claude Opus 5
parent 7a33756a7e
commit c2d9c4ad01
48 changed files with 1613 additions and 11776 deletions
@@ -1,45 +1,83 @@
{
"command": "python -c 'from openultrasast.pairs import ...; duplicate_groups / split_by_repository over the vendored catalog'",
"duplicate_groups": [
[
"openssl-cve-2023-0286",
"openssl-cve-2023-0286-general_name_cmp"
],
[
"openniw-jobs-0fc947",
"openniw-jobs-b74de3"
],
"cross_split_duplicates": [
[
"curl-cve-2023-38545",
"curl-cve-2023-38545-do_socks5"
],
[
"openssl-cve-2021-3712",
"openssl-cve-2021-3712-append_ia5"
],
[
"openssl-cve-2020-1967",
"openssl-cve-2020-1967-tls1_check_sig_alg"
],
[
"openssl-cve-2022-3786",
"openssl-cve-2022-3786-ossl_punycode_decode"
],
[
"openssl-cve-2020-1971",
"openssl-cve-2020-1971-general_name_cmp"
],
[
"etude-lexical-markdown-editorconfig-c61a8b",
"popmark-editorconfig-e5ac40"
],
[
"anastasios-pw-session-14ec1a",
"astroclaw-pw-session-1553a8"
"openniw-jobs-0fc947",
"openniw-jobs-b74de3"
],
[
"openssl-cve-2020-1967",
"openssl-cve-2020-1967-tls1_check_sig_alg"
],
[
"openssl-cve-2020-1971",
"openssl-cve-2020-1971-general_name_cmp"
],
[
"openssl-cve-2021-3712",
"openssl-cve-2021-3712-append_ia5"
],
[
"openssl-cve-2022-3602",
"openssl-cve-2022-3602-ossl_punycode_decode"
],
[
"openssl-cve-2022-3786",
"openssl-cve-2022-3786-ossl_punycode_decode"
],
[
"openssl-cve-2023-0286",
"openssl-cve-2023-0286-general_name_cmp"
]
],
"duplicate_groups": [
[
"anastasios-pw-session-14ec1a",
"astroclaw-pw-session-1553a8"
],
[
"curl-cve-2023-38545",
"curl-cve-2023-38545-do_socks5"
],
[
"etude-lexical-markdown-editorconfig-c61a8b",
"popmark-editorconfig-e5ac40"
],
[
"openniw-jobs-0fc947",
"openniw-jobs-b74de3"
],
[
"openssl-cve-2020-1967",
"openssl-cve-2020-1967-tls1_check_sig_alg"
],
[
"openssl-cve-2020-1971",
"openssl-cve-2020-1971-general_name_cmp"
],
[
"openssl-cve-2021-3712",
"openssl-cve-2021-3712-append_ia5"
],
[
"openssl-cve-2022-3602",
"openssl-cve-2022-3602-ossl_punycode_decode"
],
[
"openssl-cve-2022-3786",
"openssl-cve-2022-3786-ossl_punycode_decode"
],
[
"openssl-cve-2023-0286",
"openssl-cve-2023-0286-general_name_cmp"
]
],
"generated": "2026-09-06",
@@ -53,7 +91,7 @@
"known_limit": [
"owasp-java-hash"
],
"note": "Repositories in `straddling` have pairs on both sides of the train/holdout split, so a learning path can be taught by one file of a repository and evaluated on another. Pairs in `duplicate_groups` share a vulnerable excerpt byte for byte after the provenance header is stripped. Both cap what any detector number can mean.",
"note": "Repositories in `straddling` have pairs on both sides of the train/holdout split, so a learning path can be taught by one file of a repository and evaluated on another. Pairs in `duplicate_groups` share a vulnerable excerpt byte for byte after the provenance header is stripped. Both cap what any detector number can mean. Groups in `cross_split_duplicates` sit on both sides of the split while being the same code, so the train row teaches the holdout row exactly; forks of one project defeat the repository-level split.",
"straddling": [
[
"Contrast-Security-OSS/DjanGoat",
+15 -4
View File
@@ -881,13 +881,15 @@ fix_policy = "silent"
provenance = "agent"
split = "holdout"
review_tier = "title"
known_limit = "client_side_caller"
[[pair.expected]]
cwe = "CWE-862"
class = "missing authorization"
path = "components/KubernetesManagement.tsx"
function = "setErrorMessage"
function = "loadNamespaces"
mechanism = "missing_auth_guard"
family = "access_control"
evidence = "Fix missing auth header on namespaces API call"
[[pair]]
@@ -1091,7 +1093,7 @@ review_tier = "title"
cwe = "CWE-16"
class = "permissive default"
path = "src/presentation/http/app.ts"
function = "origin"
function = "createApplication"
mechanism = "permissive_default"
evidence = "fix: allow patch requests through cors"
@@ -1214,6 +1216,7 @@ class = "insecure direct object reference"
path = "backend/app/routers/jobs.py"
function = "get_job"
mechanism = "identity_from_request_body"
family = "access_control"
evidence = "Fix IDOR on job endpoint: case-bound jobs require case ownership"
[[pair]]
@@ -1243,6 +1246,7 @@ class = "insecure direct object reference"
path = "backend/app/routers/jobs.py"
function = "get_job"
mechanism = "identity_from_request_body"
family = "access_control"
evidence = "Fix IDOR on job endpoint: case-bound jobs require case ownership"
[[pair]]
@@ -1266,12 +1270,18 @@ provenance = "agent"
split = "train"
review_tier = "title"
[[pair.context]]
relpath = "services/web/app/src/router.mjs"
vuln = "overleaf-with-claude-exportscontroller-3980b9/context/vuln/services/web/app/src/router.mjs"
fixed = "overleaf-with-claude-exportscontroller-3980b9/context/fixed/services/web/app/src/router.mjs"
[[pair.expected]]
cwe = "CWE-639"
class = "insecure direct object reference"
path = "services/web/app/src/Features/Exports/ExportsController.mjs"
function = "exportProject"
mechanism = "identity_from_request_body"
family = "access_control"
evidence = "Fix IDOR in exports by adding token verification (Issue #31637) (#32883)"
[[pair]]
@@ -1483,6 +1493,7 @@ class = "insecure direct object reference"
path = "app/api/customers/claims/route.ts"
function = "PATCH"
mechanism = "identity_from_request_body"
family = "access_control"
evidence = "Brief 62: Fix IDOR on customer_claims PATCH/DELETE (#156)"
[[pair]]
@@ -1512,7 +1523,7 @@ class = "insecure direct object reference"
path = "app/api/customers/opportunities/route.ts"
function = "GET"
mechanism = "identity_from_request_body"
sink = "path"
family = "access_control"
evidence = "Brief 60: Fix IDOR on opportunities GET/PATCH/DELETE (missing company membership checks) (#148)"
[[pair]]
@@ -1749,7 +1760,7 @@ review_tier = "title"
cwe = "CWE-89"
class = "sql injection"
path = "src/utils.ts"
function = "normalizeDomain"
function = "getGoogleProvider"
mechanism = "source_reaches_sink"
evidence = "Fix SQL injection in search by using parameterized queries (#41)"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -5,7 +5,7 @@
// commit_url: https://github.com/Sachin-Salgar/NEW_ERP_FINAL/commit/6e9644c81ce3fb356669157182a5ef09a70c4b59
// cve:
// license: Apache-2.0
// function: origin
// function: createApplication
// relpath: src/presentation/http/app.ts
// provenance: agent
// mechanism: permissive_default
@@ -5,7 +5,7 @@
// commit_url: https://github.com/Sachin-Salgar/NEW_ERP_FINAL/commit/6e9644c81ce3fb356669157182a5ef09a70c4b59
// cve:
// license: Apache-2.0
// function: origin
// function: createApplication
// relpath: src/presentation/http/app.ts
// provenance: agent
// mechanism: permissive_default
@@ -1,4 +1,4 @@
# Provenance: HHHHHejia/openniw (fixed).
# Provenance: HHHHHejia/openniw get_job (fixed).
# repo: HHHHHejia/openniw
# commit: 0fc94733f6ca3c6b129a0a8e6d7b3acce19d5a10
# parent: 4d91ed82fa9a5ec6bf5ef96da1d221fc187700dc
@@ -9,7 +9,9 @@
# relpath: backend/app/routers/jobs.py
# provenance: agent
# mechanism: identity_from_request_body
# upstream_start: 9
@router.get("/{job_id}")
async def get_job(job_id: str, user: dict = Depends(auth.current_user)) -> dict:
job = await jobs_service.get(job_id)
if job is None:
@@ -1,4 +1,4 @@
# Provenance: HHHHHejia/openniw (vuln).
# Provenance: HHHHHejia/openniw get_job (vuln).
# repo: HHHHHejia/openniw
# commit: 4d91ed82fa9a5ec6bf5ef96da1d221fc187700dc
# parent: 4d91ed82fa9a5ec6bf5ef96da1d221fc187700dc
@@ -9,7 +9,9 @@
# relpath: backend/app/routers/jobs.py
# provenance: agent
# mechanism: identity_from_request_body
# upstream_start: 9
@router.get("/{job_id}")
async def get_job(job_id: str, user: dict = Depends(auth.current_user)) -> dict:
job = await jobs_service.get(job_id)
if job is None:
@@ -1,4 +1,4 @@
# Provenance: ziyuzuo/openniw (fixed).
# Provenance: ziyuzuo/openniw get_job (fixed).
# repo: ziyuzuo/openniw
# commit: b74de31b0a5f37e4e929ba6aa74dbe0f5e6d9a68
# parent: 98a0c392ba96cd2c4aa746dd6fc2b183f385225e
@@ -9,7 +9,9 @@
# relpath: backend/app/routers/jobs.py
# provenance: agent
# mechanism: identity_from_request_body
# upstream_start: 9
@router.get("/{job_id}")
async def get_job(job_id: str, user: dict = Depends(auth.current_user)) -> dict:
job = await jobs_service.get(job_id)
if job is None:
@@ -1,4 +1,4 @@
# Provenance: ziyuzuo/openniw (vuln).
# Provenance: ziyuzuo/openniw get_job (vuln).
# repo: ziyuzuo/openniw
# commit: 98a0c392ba96cd2c4aa746dd6fc2b183f385225e
# parent: 98a0c392ba96cd2c4aa746dd6fc2b183f385225e
@@ -9,7 +9,9 @@
# relpath: backend/app/routers/jobs.py
# provenance: agent
# mechanism: identity_from_request_body
# upstream_start: 9
@router.get("/{job_id}")
async def get_job(job_id: str, user: dict = Depends(auth.current_user)) -> dict:
job = await jobs_service.get(job_id)
if job is None:
@@ -0,0 +1,17 @@
// Provenance: ZUENS2020/overleaf-with-claude exportProject (fixed).
// repo: ZUENS2020/overleaf-with-claude
// commit: 3980b9e5806defb63e45ae2696e6c1899f3a8431
// parent: 5a886aa9fb9fe4e2203f3cb1d62c91f4be0525ed
// commit_url: https://github.com/ZUENS2020/overleaf-with-claude/commit/3980b9e5806defb63e45ae2696e6c1899f3a8431
// cve:
// license: AGPL-3.0
// function: exportProject
// relpath: services/web/app/src/router.mjs
// provenance: agent
// mechanism: identity_from_request_body
webRouter.post(
'/project/:project_id/export/:brand_variation_id',
AuthorizationMiddleware.ensureUserCanWriteProjectContent,
ExportsController.exportProject
)
@@ -0,0 +1,17 @@
// Provenance: ZUENS2020/overleaf-with-claude exportProject (vuln).
// repo: ZUENS2020/overleaf-with-claude
// commit: 5a886aa9fb9fe4e2203f3cb1d62c91f4be0525ed
// parent: 5a886aa9fb9fe4e2203f3cb1d62c91f4be0525ed
// commit_url: https://github.com/ZUENS2020/overleaf-with-claude/commit/3980b9e5806defb63e45ae2696e6c1899f3a8431
// cve:
// license: AGPL-3.0
// function: exportProject
// relpath: services/web/app/src/router.mjs
// provenance: agent
// mechanism: identity_from_request_body
webRouter.post(
'/project/:project_id/export/:brand_variation_id',
AuthorizationMiddleware.ensureUserCanWriteProjectContent,
ExportsController.exportProject
)
@@ -1,4 +1,4 @@
// Provenance: ZUENS2020/overleaf-with-claude (fixed).
// Provenance: ZUENS2020/overleaf-with-claude exportProject (fixed).
// repo: ZUENS2020/overleaf-with-claude
// commit: 3980b9e5806defb63e45ae2696e6c1899f3a8431
// parent: 5a886aa9fb9fe4e2203f3cb1d62c91f4be0525ed
@@ -9,8 +9,9 @@
// relpath: services/web/app/src/Features/Exports/ExportsController.mjs
// provenance: agent
// mechanism: identity_from_request_body
// upstream_start: 1
import { expressify } from '@overleaf/promise-utils'
from '@overleaf/promise-utils'
import SessionManager from '../Authentication/SessionManager.mjs'
import logger from '@overleaf/logger'
import OError from '@overleaf/o-error'
@@ -80,3 +81,5 @@ async function exportProject(req, res, next) {
throw err
}
}
exportProject: expressify(exportProject),
@@ -1,4 +1,4 @@
// Provenance: ZUENS2020/overleaf-with-claude (vuln).
// Provenance: ZUENS2020/overleaf-with-claude exportProject (vuln).
// repo: ZUENS2020/overleaf-with-claude
// commit: 5a886aa9fb9fe4e2203f3cb1d62c91f4be0525ed
// parent: 5a886aa9fb9fe4e2203f3cb1d62c91f4be0525ed
@@ -9,8 +9,9 @@
// relpath: services/web/app/src/Features/Exports/ExportsController.mjs
// provenance: agent
// mechanism: identity_from_request_body
// upstream_start: 1
import { expressify } from '@overleaf/promise-utils'
from '@overleaf/promise-utils'
import SessionManager from '../Authentication/SessionManager.mjs'
import logger from '@overleaf/logger'
import OError from '@overleaf/o-error'
@@ -78,3 +79,5 @@ async function exportProject(req, res, next) {
throw err
}
}
exportProject: expressify(exportProject),
+22 -6
View File
@@ -904,7 +904,7 @@ repo = "amitknigam84/lensops"
parent = "9b87ecc3a44a7fe2a5185cca1fe52441647d5748"
commit = "fe49109aedfe21dea4d24d4d9f2fa3f1ecfb6c84"
path = "components/KubernetesManagement.tsx"
mode = "hunk"
mode = "handler_context"
language = "typescript"
relpath = "components/KubernetesManagement.tsx"
license = "MIT"
@@ -920,6 +920,11 @@ trailer = "Co-Authored-By: Claude Sonnet 4.6 <[email protected]>"
reviewer = "pending"
evidence = "Fix missing auth header on namespaces API call"
commit_url = "https://github.com/amitknigam84/lensops/commit/fe49109aedfe21dea4d24d4d9f2fa3f1ecfb6c84"
family = "access_control"
function = "loadNamespaces"
line = 805
fix_line = 805
known_limit = "client_side_caller"
split = "holdout"
[[recipe]]
@@ -1211,7 +1216,7 @@ repo = "HHHHHejia/openniw"
parent = "4d91ed82fa9a5ec6bf5ef96da1d221fc187700dc"
commit = "0fc94733f6ca3c6b129a0a8e6d7b3acce19d5a10"
path = "backend/app/routers/jobs.py"
mode = "hunk"
mode = "handler_context"
language = "python"
relpath = "backend/app/routers/jobs.py"
license = "MIT"
@@ -1227,6 +1232,8 @@ trailer = "Co-Authored-By: Claude Fable 5 <[email protected]>"
reviewer = "pending"
evidence = "Fix IDOR on job endpoint: case-bound jobs require case ownership"
commit_url = "https://github.com/HHHHHejia/openniw/commit/0fc94733f6ca3c6b129a0a8e6d7b3acce19d5a10"
family = "access_control"
function = "get_job"
split = "train"
[[recipe]]
@@ -1236,7 +1243,7 @@ repo = "ziyuzuo/openniw"
parent = "98a0c392ba96cd2c4aa746dd6fc2b183f385225e"
commit = "b74de31b0a5f37e4e929ba6aa74dbe0f5e6d9a68"
path = "backend/app/routers/jobs.py"
mode = "hunk"
mode = "handler_context"
language = "python"
relpath = "backend/app/routers/jobs.py"
license = "MIT"
@@ -1252,6 +1259,8 @@ trailer = "Co-Authored-By: Claude Fable 5 <[email protected]>"
reviewer = "pending"
evidence = "Fix IDOR on job endpoint: case-bound jobs require case ownership"
commit_url = "https://github.com/ziyuzuo/openniw/commit/b74de31b0a5f37e4e929ba6aa74dbe0f5e6d9a68"
family = "access_control"
function = "get_job"
split = "holdout"
[[recipe]]
@@ -1261,7 +1270,7 @@ repo = "ZUENS2020/overleaf-with-claude"
parent = "5a886aa9fb9fe4e2203f3cb1d62c91f4be0525ed"
commit = "3980b9e5806defb63e45ae2696e6c1899f3a8431"
path = "services/web/app/src/Features/Exports/ExportsController.mjs"
mode = "hunk"
mode = "handler_context"
language = "javascript"
relpath = "services/web/app/src/Features/Exports/ExportsController.mjs"
license = "AGPL-3.0"
@@ -1277,6 +1286,9 @@ trailer = "Co-authored-by: briangough <[email protected]
reviewer = "pending"
evidence = "Fix IDOR in exports by adding token verification (Issue #31637) (#32883)"
commit_url = "https://github.com/ZUENS2020/overleaf-with-claude/commit/3980b9e5806defb63e45ae2696e6c1899f3a8431"
family = "access_control"
function = "exportProject"
context = ["services/web/app/src/router.mjs"]
split = "train"
[[recipe]]
@@ -1468,7 +1480,7 @@ repo = "bigfivefoods/supplieradvisor-mvp"
parent = "a7a9bc260d649ea2e582c07435bfd7d93100647b"
commit = "387bcf40e984a2ee1832ab7beabf0284c5b627d8"
path = "app/api/customers/claims/route.ts"
mode = "hunk"
mode = "handler_context"
language = "typescript"
relpath = "app/api/customers/claims/route.ts"
license = "MIT"
@@ -1484,6 +1496,8 @@ trailer = "Co-authored-by: bigfivefoods <[email protected]
reviewer = "pending"
evidence = "Brief 62: Fix IDOR on customer_claims PATCH/DELETE (#156)"
commit_url = "https://github.com/bigfivefoods/supplieradvisor-mvp/commit/387bcf40e984a2ee1832ab7beabf0284c5b627d8"
family = "access_control"
function = "PATCH"
split = "holdout"
[[recipe]]
@@ -1493,7 +1507,7 @@ repo = "bigfivefoods/supplieradvisor-mvp"
parent = "8558538471e4fb10856f8d9a17d2be5ab5c4fb9c"
commit = "a69e8f27c0f4d402573d16f0612b51155bbc46e1"
path = "app/api/customers/opportunities/route.ts"
mode = "hunk"
mode = "handler_context"
language = "typescript"
relpath = "app/api/customers/opportunities/route.ts"
license = "MIT"
@@ -1509,6 +1523,8 @@ trailer = "Co-authored-by: bigfivefoods <[email protected]
reviewer = "pending"
evidence = "Brief 60: Fix IDOR on opportunities GET/PATCH/DELETE (missing company membership checks) (#148)"
commit_url = "https://github.com/bigfivefoods/supplieradvisor-mvp/commit/a69e8f27c0f4d402573d16f0612b51155bbc46e1"
family = "access_control"
function = "GET"
split = "train"
[[recipe]]
@@ -1,4 +1,4 @@
// Provenance: bigfivefoods/supplieradvisor-mvp (fixed).
// Provenance: bigfivefoods/supplieradvisor-mvp PATCH (fixed).
// repo: bigfivefoods/supplieradvisor-mvp
// commit: 387bcf40e984a2ee1832ab7beabf0284c5b627d8
// parent: a7a9bc260d649ea2e582c07435bfd7d93100647b
@@ -9,6 +9,7 @@
// relpath: app/api/customers/claims/route.ts
// provenance: agent
// mechanism: identity_from_request_body
// upstream_start: 90
export async function PATCH(request: NextRequest) {
try {
@@ -1,4 +1,4 @@
// Provenance: bigfivefoods/supplieradvisor-mvp (vuln).
// Provenance: bigfivefoods/supplieradvisor-mvp PATCH (vuln).
// repo: bigfivefoods/supplieradvisor-mvp
// commit: a7a9bc260d649ea2e582c07435bfd7d93100647b
// parent: a7a9bc260d649ea2e582c07435bfd7d93100647b
@@ -9,6 +9,7 @@
// relpath: app/api/customers/claims/route.ts
// provenance: agent
// mechanism: identity_from_request_body
// upstream_start: 90
export async function PATCH(request: NextRequest) {
try {
@@ -1,4 +1,4 @@
// Provenance: bigfivefoods/supplieradvisor-mvp (fixed).
// Provenance: bigfivefoods/supplieradvisor-mvp GET (fixed).
// repo: bigfivefoods/supplieradvisor-mvp
// commit: a69e8f27c0f4d402573d16f0612b51155bbc46e1
// parent: 8558538471e4fb10856f8d9a17d2be5ab5c4fb9c
@@ -9,6 +9,7 @@
// relpath: app/api/customers/opportunities/route.ts
// provenance: agent
// mechanism: identity_from_request_body
// upstream_start: 89
export async function GET(request: NextRequest) {
try {
@@ -1,4 +1,4 @@
// Provenance: bigfivefoods/supplieradvisor-mvp (vuln).
// Provenance: bigfivefoods/supplieradvisor-mvp GET (vuln).
// repo: bigfivefoods/supplieradvisor-mvp
// commit: 8558538471e4fb10856f8d9a17d2be5ab5c4fb9c
// parent: 8558538471e4fb10856f8d9a17d2be5ab5c4fb9c
@@ -9,6 +9,7 @@
// relpath: app/api/customers/opportunities/route.ts
// provenance: agent
// mechanism: identity_from_request_body
// upstream_start: 89
export async function GET(request: NextRequest) {
try {
@@ -12,8 +12,8 @@
{"class": "prototype pollution", "commit": "c61a8bf78fe20d6ae0eab2e4b8b02004508525ed", "commit_url": "https://github.com/dayflower/etude-lexical-markdown/commit/c61a8bf78fe20d6ae0eab2e4b8b02004508525ed", "cve": "", "cwe": "CWE-1321", "file": "etude-lexical-markdown-editorconfig-c61a8b/fixed.ts", "function": "deepMergeInto", "id": "etude-lexical-markdown-editorconfig-c61a8b:fixed", "label": "fixed", "license": "MIT", "mechanism": "prototype_pollution", "pair": "etude-lexical-markdown-editorconfig-c61a8b", "project": "etude-lexical-markdown", "provenance": "agent", "relpath": "src/config/editorConfig.ts", "repo": "dayflower/etude-lexical-markdown", "review_tier": "title", "split": "train", "year": 2026}
{"class": "prototype pollution", "commit": "1c865815d05b072f4973388c3ac66b6696d9efde", "commit_url": "https://github.com/dayflower/etude-lexical-markdown/commit/db4f54b1ab80ad596de6425e84f90aa781c49999", "cve": "", "cwe": "CWE-1321", "file": "etude-lexical-markdown-editorconfig-db4f54/vuln.ts", "function": "deepMergeInto", "id": "etude-lexical-markdown-editorconfig-db4f54:vuln", "label": "vuln", "license": "MIT", "mechanism": "prototype_pollution", "pair": "etude-lexical-markdown-editorconfig-db4f54", "project": "etude-lexical-markdown", "provenance": "agent", "relpath": "src/config/editorConfig.ts", "repo": "dayflower/etude-lexical-markdown", "review_tier": "title", "split": "holdout", "year": 2026}
{"class": "prototype pollution", "commit": "db4f54b1ab80ad596de6425e84f90aa781c49999", "commit_url": "https://github.com/dayflower/etude-lexical-markdown/commit/db4f54b1ab80ad596de6425e84f90aa781c49999", "cve": "", "cwe": "CWE-1321", "file": "etude-lexical-markdown-editorconfig-db4f54/fixed.ts", "function": "deepMergeInto", "id": "etude-lexical-markdown-editorconfig-db4f54:fixed", "label": "fixed", "license": "MIT", "mechanism": "prototype_pollution", "pair": "etude-lexical-markdown-editorconfig-db4f54", "project": "etude-lexical-markdown", "provenance": "agent", "relpath": "src/config/editorConfig.ts", "repo": "dayflower/etude-lexical-markdown", "review_tier": "title", "split": "holdout", "year": 2026}
{"class": "missing authorization", "commit": "9b87ecc3a44a7fe2a5185cca1fe52441647d5748", "commit_url": "https://github.com/amitknigam84/lensops/commit/fe49109aedfe21dea4d24d4d9f2fa3f1ecfb6c84", "cve": "", "cwe": "CWE-862", "file": "lensops-kubernetesmanagement-fe4910/vuln.tsx", "function": "setErrorMessage", "id": "lensops-kubernetesmanagement-fe4910:vuln", "label": "vuln", "license": "MIT", "mechanism": "missing_auth_guard", "pair": "lensops-kubernetesmanagement-fe4910", "project": "lensops", "provenance": "agent", "relpath": "components/KubernetesManagement.tsx", "repo": "amitknigam84/lensops", "review_tier": "title", "split": "holdout", "year": 2026}
{"class": "missing authorization", "commit": "fe49109aedfe21dea4d24d4d9f2fa3f1ecfb6c84", "commit_url": "https://github.com/amitknigam84/lensops/commit/fe49109aedfe21dea4d24d4d9f2fa3f1ecfb6c84", "cve": "", "cwe": "CWE-862", "file": "lensops-kubernetesmanagement-fe4910/fixed.tsx", "function": "setErrorMessage", "id": "lensops-kubernetesmanagement-fe4910:fixed", "label": "fixed", "license": "MIT", "mechanism": "missing_auth_guard", "pair": "lensops-kubernetesmanagement-fe4910", "project": "lensops", "provenance": "agent", "relpath": "components/KubernetesManagement.tsx", "repo": "amitknigam84/lensops", "review_tier": "title", "split": "holdout", "year": 2026}
{"class": "missing authorization", "commit": "9b87ecc3a44a7fe2a5185cca1fe52441647d5748", "commit_url": "https://github.com/amitknigam84/lensops/commit/fe49109aedfe21dea4d24d4d9f2fa3f1ecfb6c84", "cve": "", "cwe": "CWE-862", "file": "lensops-kubernetesmanagement-fe4910/vuln.tsx", "function": "loadNamespaces", "id": "lensops-kubernetesmanagement-fe4910:vuln", "label": "vuln", "license": "MIT", "mechanism": "missing_auth_guard", "pair": "lensops-kubernetesmanagement-fe4910", "project": "lensops", "provenance": "agent", "relpath": "components/KubernetesManagement.tsx", "repo": "amitknigam84/lensops", "review_tier": "title", "split": "holdout", "year": 2026}
{"class": "missing authorization", "commit": "fe49109aedfe21dea4d24d4d9f2fa3f1ecfb6c84", "commit_url": "https://github.com/amitknigam84/lensops/commit/fe49109aedfe21dea4d24d4d9f2fa3f1ecfb6c84", "cve": "", "cwe": "CWE-862", "file": "lensops-kubernetesmanagement-fe4910/fixed.tsx", "function": "loadNamespaces", "id": "lensops-kubernetesmanagement-fe4910:fixed", "label": "fixed", "license": "MIT", "mechanism": "missing_auth_guard", "pair": "lensops-kubernetesmanagement-fe4910", "project": "lensops", "provenance": "agent", "relpath": "components/KubernetesManagement.tsx", "repo": "amitknigam84/lensops", "review_tier": "title", "split": "holdout", "year": 2026}
{"class": "open redirect", "commit": "fec6f024ce72c9c47b61cbac6ce7a35cb2280602", "commit_url": "https://github.com/navaneethbv/market-cap/commit/4f8ae9be5519ced5a975f1190e3e4bc43f84e4d6", "cve": "", "cwe": "CWE-601", "file": "market-cap-route-4f8ae9/vuln.ts", "function": "GET", "id": "market-cap-route-4f8ae9:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "market-cap-route-4f8ae9", "project": "market-cap", "provenance": "agent", "relpath": "app/auth/confirm/route.ts", "repo": "navaneethbv/market-cap", "review_tier": "title", "split": "train", "year": 2026}
{"class": "open redirect", "commit": "4f8ae9be5519ced5a975f1190e3e4bc43f84e4d6", "commit_url": "https://github.com/navaneethbv/market-cap/commit/4f8ae9be5519ced5a975f1190e3e4bc43f84e4d6", "cve": "", "cwe": "CWE-601", "file": "market-cap-route-4f8ae9/fixed.ts", "function": "GET", "id": "market-cap-route-4f8ae9:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "market-cap-route-4f8ae9", "project": "market-cap", "provenance": "agent", "relpath": "app/auth/confirm/route.ts", "repo": "navaneethbv/market-cap", "review_tier": "title", "split": "train", "year": 2026}
{"class": "permissive default", "commit": "c074fc7413338aefcbea3170ef4d3ba486a8a4b7", "commit_url": "https://github.com/alexshao3/melodix/commit/089931ef71ea1e5b7586520baf1a5498e2f05b13", "cve": "", "cwe": "CWE-16", "file": "melodix-playerprovider-089931/vuln.tsx", "function": "PlayerProvider", "id": "melodix-playerprovider-089931:vuln", "label": "vuln", "license": "MIT", "mechanism": "permissive_default", "pair": "melodix-playerprovider-089931", "project": "melodix", "provenance": "agent", "relpath": "apps/miniapp/src/components/PlayerProvider.tsx", "repo": "alexshao3/melodix", "review_tier": "title", "split": "holdout", "year": 2026}
@@ -24,8 +24,8 @@
{"class": "prototype pollution", "commit": "4d4a5b063278c8b6697c3faae133d7c39fcd01c0", "commit_url": "https://github.com/andrebubniak/money-track/commit/4d4a5b063278c8b6697c3faae133d7c39fcd01c0", "cve": "", "cwe": "CWE-1321", "file": "money-track-category-icons-4d4a5b/fixed.ts", "function": "isCategoryIcon", "id": "money-track-category-icons-4d4a5b:fixed", "label": "fixed", "license": "MIT", "mechanism": "prototype_pollution", "pair": "money-track-category-icons-4d4a5b", "project": "money-track", "provenance": "agent", "relpath": "src/lib/category-icons.ts", "repo": "andrebubniak/money-track", "review_tier": "title", "split": "holdout", "year": 2026}
{"class": "prototype pollution", "commit": "d899b40d3466f2ffbe9f01c4d49267412401f078", "commit_url": "https://github.com/DavideCarvalho/nestjs-filter/commit/aa0e600d05997ea3d77538ea1c2db427c439f2e8", "cve": "", "cwe": "CWE-1321", "file": "nestjs-filter-normalizer-aa0e60/vuln.ts", "function": "normalizeInput", "id": "nestjs-filter-normalizer-aa0e60:vuln", "label": "vuln", "license": "MIT", "mechanism": "prototype_pollution", "pair": "nestjs-filter-normalizer-aa0e60", "project": "nestjs-filter", "provenance": "agent", "relpath": "packages/core/src/input/normalizer.ts", "repo": "DavideCarvalho/nestjs-filter", "review_tier": "title", "split": "train", "year": 2026}
{"class": "prototype pollution", "commit": "aa0e600d05997ea3d77538ea1c2db427c439f2e8", "commit_url": "https://github.com/DavideCarvalho/nestjs-filter/commit/aa0e600d05997ea3d77538ea1c2db427c439f2e8", "cve": "", "cwe": "CWE-1321", "file": "nestjs-filter-normalizer-aa0e60/fixed.ts", "function": "normalizeInput", "id": "nestjs-filter-normalizer-aa0e60:fixed", "label": "fixed", "license": "MIT", "mechanism": "prototype_pollution", "pair": "nestjs-filter-normalizer-aa0e60", "project": "nestjs-filter", "provenance": "agent", "relpath": "packages/core/src/input/normalizer.ts", "repo": "DavideCarvalho/nestjs-filter", "review_tier": "title", "split": "train", "year": 2026}
{"class": "permissive default", "commit": "08976c5d7a07b0663669b875b4507446199f85d7", "commit_url": "https://github.com/Sachin-Salgar/NEW_ERP_FINAL/commit/6e9644c81ce3fb356669157182a5ef09a70c4b59", "cve": "", "cwe": "CWE-16", "file": "new-erp-final-app-6e9644/vuln.ts", "function": "origin", "id": "new-erp-final-app-6e9644:vuln", "label": "vuln", "license": "Apache-2.0", "mechanism": "permissive_default", "pair": "new-erp-final-app-6e9644", "project": "NEW_ERP_FINAL", "provenance": "agent", "relpath": "src/presentation/http/app.ts", "repo": "Sachin-Salgar/NEW_ERP_FINAL", "review_tier": "title", "split": "holdout", "year": 2026}
{"class": "permissive default", "commit": "6e9644c81ce3fb356669157182a5ef09a70c4b59", "commit_url": "https://github.com/Sachin-Salgar/NEW_ERP_FINAL/commit/6e9644c81ce3fb356669157182a5ef09a70c4b59", "cve": "", "cwe": "CWE-16", "file": "new-erp-final-app-6e9644/fixed.ts", "function": "origin", "id": "new-erp-final-app-6e9644:fixed", "label": "fixed", "license": "Apache-2.0", "mechanism": "permissive_default", "pair": "new-erp-final-app-6e9644", "project": "NEW_ERP_FINAL", "provenance": "agent", "relpath": "src/presentation/http/app.ts", "repo": "Sachin-Salgar/NEW_ERP_FINAL", "review_tier": "title", "split": "holdout", "year": 2026}
{"class": "permissive default", "commit": "08976c5d7a07b0663669b875b4507446199f85d7", "commit_url": "https://github.com/Sachin-Salgar/NEW_ERP_FINAL/commit/6e9644c81ce3fb356669157182a5ef09a70c4b59", "cve": "", "cwe": "CWE-16", "file": "new-erp-final-app-6e9644/vuln.ts", "function": "createApplication", "id": "new-erp-final-app-6e9644:vuln", "label": "vuln", "license": "Apache-2.0", "mechanism": "permissive_default", "pair": "new-erp-final-app-6e9644", "project": "NEW_ERP_FINAL", "provenance": "agent", "relpath": "src/presentation/http/app.ts", "repo": "Sachin-Salgar/NEW_ERP_FINAL", "review_tier": "title", "split": "holdout", "year": 2026}
{"class": "permissive default", "commit": "6e9644c81ce3fb356669157182a5ef09a70c4b59", "commit_url": "https://github.com/Sachin-Salgar/NEW_ERP_FINAL/commit/6e9644c81ce3fb356669157182a5ef09a70c4b59", "cve": "", "cwe": "CWE-16", "file": "new-erp-final-app-6e9644/fixed.ts", "function": "createApplication", "id": "new-erp-final-app-6e9644:fixed", "label": "fixed", "license": "Apache-2.0", "mechanism": "permissive_default", "pair": "new-erp-final-app-6e9644", "project": "NEW_ERP_FINAL", "provenance": "agent", "relpath": "src/presentation/http/app.ts", "repo": "Sachin-Salgar/NEW_ERP_FINAL", "review_tier": "title", "split": "holdout", "year": 2026}
{"class": "server-side request forgery", "commit": "0c37bfd9e5292b15b1d7af51f52793d9bdf1cef0", "commit_url": "https://github.com/Irnhakim/nimestream/commit/d17f535bf3012041b056ff3aa991ff6cfa198e43", "cve": "", "cwe": "CWE-918", "file": "nimestream-route-d17f53/vuln.js", "function": "GET", "id": "nimestream-route-d17f53:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "nimestream-route-d17f53", "project": "nimestream", "provenance": "agent", "relpath": "app/api/img/route.js", "repo": "Irnhakim/nimestream", "review_tier": "title", "split": "train", "year": 2026}
{"class": "server-side request forgery", "commit": "d17f535bf3012041b056ff3aa991ff6cfa198e43", "commit_url": "https://github.com/Irnhakim/nimestream/commit/d17f535bf3012041b056ff3aa991ff6cfa198e43", "cve": "", "cwe": "CWE-918", "file": "nimestream-route-d17f53/fixed.js", "function": "GET", "id": "nimestream-route-d17f53:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "nimestream-route-d17f53", "project": "nimestream", "provenance": "agent", "relpath": "app/api/img/route.js", "repo": "Irnhakim/nimestream", "review_tier": "title", "split": "train", "year": 2026}
{"class": "insecure direct object reference", "commit": "4d91ed82fa9a5ec6bf5ef96da1d221fc187700dc", "commit_url": "https://github.com/HHHHHejia/openniw/commit/0fc94733f6ca3c6b129a0a8e6d7b3acce19d5a10", "cve": "", "cwe": "CWE-639", "file": "openniw-jobs-0fc947/vuln.py", "function": "get_job", "id": "openniw-jobs-0fc947:vuln", "label": "vuln", "license": "MIT", "mechanism": "identity_from_request_body", "pair": "openniw-jobs-0fc947", "project": "openniw", "provenance": "agent", "relpath": "backend/app/routers/jobs.py", "repo": "HHHHHejia/openniw", "review_tier": "title", "split": "train", "year": 2026}
@@ -52,7 +52,7 @@
{"class": "path traversal", "commit": "c3a0787080489b30e2ed0abfac03b51e45da184b", "commit_url": "https://github.com/D-sorganization/UpstreamDrift/commit/c3a0787080489b30e2ed0abfac03b51e45da184b", "cve": "", "cwe": "CWE-22", "file": "upstreamdrift-local-server-c3a078/fixed.py", "function": "_find_logo_file", "id": "upstreamdrift-local-server-c3a078:fixed", "label": "fixed", "license": "MIT", "mechanism": "path_join_user_input", "pair": "upstreamdrift-local-server-c3a078", "project": "UpstreamDrift", "provenance": "agent", "relpath": "src/api/local_server.py", "repo": "D-sorganization/UpstreamDrift", "review_tier": "title", "split": "train", "year": 2026}
{"class": "server-side request forgery", "commit": "934b939f1cca5259eaf807db3319eb97b90d8d80", "commit_url": "https://github.com/Ahkeratmehilaiset/waggledance-swarm/commit/9a454843aa35332ff2eb3106023eee0c4b5e444b", "cve": "", "cwe": "CWE-918", "file": "waggledance-swarm-ssrf-host-guard-9a4548/vuln.py", "function": "normalize_allowed_hosts", "id": "waggledance-swarm-ssrf-host-guard-9a4548:vuln", "label": "vuln", "license": "Apache-2.0", "mechanism": "source_reaches_sink", "pair": "waggledance-swarm-ssrf-host-guard-9a4548", "project": "waggledance-swarm", "provenance": "agent", "relpath": "waggledance/core/v3_13_0/ssrf_host_guard.py", "repo": "Ahkeratmehilaiset/waggledance-swarm", "review_tier": "title", "split": "train", "year": 2026}
{"class": "server-side request forgery", "commit": "9a454843aa35332ff2eb3106023eee0c4b5e444b", "commit_url": "https://github.com/Ahkeratmehilaiset/waggledance-swarm/commit/9a454843aa35332ff2eb3106023eee0c4b5e444b", "cve": "", "cwe": "CWE-918", "file": "waggledance-swarm-ssrf-host-guard-9a4548/fixed.py", "function": "normalize_allowed_hosts", "id": "waggledance-swarm-ssrf-host-guard-9a4548:fixed", "label": "fixed", "license": "Apache-2.0", "mechanism": "source_reaches_sink", "pair": "waggledance-swarm-ssrf-host-guard-9a4548", "project": "waggledance-swarm", "provenance": "agent", "relpath": "waggledance/core/v3_13_0/ssrf_host_guard.py", "repo": "Ahkeratmehilaiset/waggledance-swarm", "review_tier": "title", "split": "train", "year": 2026}
{"class": "sql injection", "commit": "eab923ad3099c01b002fbd6be7cf994614bbdee4", "commit_url": "https://github.com/G4brym/workers-research/commit/209444eb61cc564c2d6b43267256f85fe3e5e30e", "cve": "", "cwe": "CWE-89", "file": "workers-research-utils-209444/vuln.ts", "function": "normalizeDomain", "id": "workers-research-utils-209444:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "workers-research-utils-209444", "project": "workers-research", "provenance": "agent", "relpath": "src/utils.ts", "repo": "G4brym/workers-research", "review_tier": "title", "split": "holdout", "year": 2026}
{"class": "sql injection", "commit": "209444eb61cc564c2d6b43267256f85fe3e5e30e", "commit_url": "https://github.com/G4brym/workers-research/commit/209444eb61cc564c2d6b43267256f85fe3e5e30e", "cve": "", "cwe": "CWE-89", "file": "workers-research-utils-209444/fixed.ts", "function": "normalizeDomain", "id": "workers-research-utils-209444:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "workers-research-utils-209444", "project": "workers-research", "provenance": "agent", "relpath": "src/utils.ts", "repo": "G4brym/workers-research", "review_tier": "title", "split": "holdout", "year": 2026}
{"class": "sql injection", "commit": "eab923ad3099c01b002fbd6be7cf994614bbdee4", "commit_url": "https://github.com/G4brym/workers-research/commit/209444eb61cc564c2d6b43267256f85fe3e5e30e", "cve": "", "cwe": "CWE-89", "file": "workers-research-utils-209444/vuln.ts", "function": "getGoogleProvider", "id": "workers-research-utils-209444:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "workers-research-utils-209444", "project": "workers-research", "provenance": "agent", "relpath": "src/utils.ts", "repo": "G4brym/workers-research", "review_tier": "title", "split": "holdout", "year": 2026}
{"class": "sql injection", "commit": "209444eb61cc564c2d6b43267256f85fe3e5e30e", "commit_url": "https://github.com/G4brym/workers-research/commit/209444eb61cc564c2d6b43267256f85fe3e5e30e", "cve": "", "cwe": "CWE-89", "file": "workers-research-utils-209444/fixed.ts", "function": "getGoogleProvider", "id": "workers-research-utils-209444:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "workers-research-utils-209444", "project": "workers-research", "provenance": "agent", "relpath": "src/utils.ts", "repo": "G4brym/workers-research", "review_tier": "title", "split": "holdout", "year": 2026}
{"class": "permissive default", "commit": "2604269350bff51f58572c133b45010cb0976a5a", "commit_url": "https://github.com/zoonk/zoonk/commit/bfbf670ccb41d63c407d32c51bd26e626a174f31", "cve": "", "cwe": "CWE-16", "file": "zoonk-origin-bfbf67/vuln.ts", "function": "getAllowedHosts", "id": "zoonk-origin-bfbf67:vuln", "label": "vuln", "license": "MIT", "mechanism": "permissive_default", "pair": "zoonk-origin-bfbf67", "project": "zoonk", "provenance": "agent", "relpath": "packages/utils/src/origin.ts", "repo": "zoonk/zoonk", "review_tier": "title", "split": "train", "year": 2026}
{"class": "permissive default", "commit": "bfbf670ccb41d63c407d32c51bd26e626a174f31", "commit_url": "https://github.com/zoonk/zoonk/commit/bfbf670ccb41d63c407d32c51bd26e626a174f31", "cve": "", "cwe": "CWE-16", "file": "zoonk-origin-bfbf67/fixed.ts", "function": "getAllowedHosts", "id": "zoonk-origin-bfbf67:fixed", "label": "fixed", "license": "MIT", "mechanism": "permissive_default", "pair": "zoonk-origin-bfbf67", "project": "zoonk", "provenance": "agent", "relpath": "packages/utils/src/origin.ts", "repo": "zoonk/zoonk", "review_tier": "title", "split": "train", "year": 2026}
@@ -5,7 +5,7 @@
// commit_url: https://github.com/G4brym/workers-research/commit/209444eb61cc564c2d6b43267256f85fe3e5e30e
// cve:
// license: MIT
// function: normalizeDomain
// function: getGoogleProvider
// relpath: src/utils.ts
// provenance: agent
// mechanism: source_reaches_sink
@@ -5,7 +5,7 @@
// commit_url: https://github.com/G4brym/workers-research/commit/209444eb61cc564c2d6b43267256f85fe3e5e30e
// cve:
// license: MIT
// function: normalizeDomain
// function: getGoogleProvider
// relpath: src/utils.ts
// provenance: agent
// mechanism: source_reaches_sink
+18
View File
@@ -37,6 +37,14 @@ def excerpt_rel(recipe: dict[str, object]) -> tuple[str, str]:
return f"{name}/vuln{ext}", f"{name}/fixed{ext}"
def context_rel(recipe: dict[str, object]) -> list[tuple[str, str, str]]:
"""`(relpath, vuln document, fixed document)` for every document that registers the handler from outside its module."""
name = str(recipe["name"])
value = recipe.get("context") or ()
relpaths = (value,) if isinstance(value, str) else tuple(str(item) for item in value)
return [(relpath, f"{name}/context/vuln/{relpath}", f"{name}/context/fixed/{relpath}") for relpath in relpaths]
def _q(value: object) -> str:
return str(value).replace("\\", "\\\\").replace('"', '\\"')
@@ -85,6 +93,14 @@ def catalog_text(slice_name: str, recipes: list[dict[str, object]]) -> str:
lines.append(f'reviewer = "{_q(reviewer)}"')
if recipe.get("known_limit"):
lines.append(f'known_limit = "{_q(recipe["known_limit"])}"')
for relpath, context_vuln, context_fixed in context_rel(recipe):
lines += [
"",
"[[pair.context]]",
f'relpath = "{_q(relpath)}"',
f'vuln = "{_q(context_vuln)}"',
f'fixed = "{_q(context_fixed)}"',
]
lines += [
"",
"[[pair.expected]]",
@@ -95,6 +111,8 @@ def catalog_text(slice_name: str, recipes: list[dict[str, object]]) -> str:
if recipe.get("function"):
lines.append(f'function = "{_q(recipe["function"])}"')
lines.append(f'mechanism = "{_q(recipe.get("mechanism", "other"))}"')
if recipe.get("family"):
lines.append(f'family = "{_q(recipe["family"])}"') # Req 1: the family routes the work, never a CWE lookup
if recipe.get("rule_id"):
lines.append(f'rule_id = "{_q(recipe["rule_id"])}"')
if recipe.get("sink"):
+312 -30
View File
@@ -20,9 +20,11 @@ from __future__ import annotations
import argparse
import difflib
import json
import re
import sys
import tomllib
from collections.abc import Sequence
from pathlib import Path
from typing import Any
from urllib.parse import urlparse
@@ -53,9 +55,16 @@ _SUFFIX_LANGUAGE = {
".py": "python",
".pl": "perl",
".pm": "perl",
".yml": "yaml",
".yaml": "yaml",
}
_HASH_COMMENT = {"python", "perl"}
_HASH_COMMENT = {"python", "perl", "yaml"}
_CONTEXT_LANGUAGES = {"yaml", "javascript", "typescript", "python"}
_DEF_RE = re.compile(r"^(\s*)(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(")
_CLASS_RE = re.compile(r"^(\s*)class\s+([A-Za-z_]\w*)\s*[(:]")
_COMMENTED_CODE = re.compile(r"^#\s*(?:@|(?:async\s+)?def\s|class\s|function\s)")
_CONTROL_HEAD = re.compile(r"^(?:else\s+)?(?:if|for|while|switch|catch|do|try|finally)\b")
_INDENTED = {"python", "yaml"}
class RecipeError(ValueError):
@@ -184,38 +193,230 @@ def extract_python_block(source: str, function_name: str) -> str:
if match is None or match.group(2) != function_name:
continue
indent = len(match.group(1))
start = index
while (
start > 0 and lines[start - 1].lstrip().startswith("@") and (len(lines[start - 1]) - len(lines[start - 1].lstrip())) == indent
):
start -= 1 # decorators belong to the function
start = _decorator_start(lines, index, indent)
end = _python_block_end(masked, index, indent)
return "".join(lines[start:end]).rstrip("\n") + "\n"
raise RecipeError(f"function not found: {function_name}")
def extract_handler_context(source: str, function_name: str, language: str = "c") -> str:
"""The named function (with its decorators) followed by every statement outside it that names the function as an
identifier: `app.add_url_rule(..., view_func=fn)`, `router.get('/x', fn)`, `path('x', fn)`, `app.use(fn)`. Comment and
string mentions never count (the masked text is searched). One excerpt per side, so guards living in decorators,
routers and middleware are visible to the teacher and the scorer (authorization-obligations, Req 7.1)."""
body = extract_function(source, function_name, language)
def _decorator_start(lines: Sequence[str], index: int, indent: int) -> int:
"""First line of the decorator run above ``index``: multi-line decorators and comments included.
A commented-out decorator is not noise: `#@token_required` above a handler is the whole bug in an absence row, and
an excerpt that starts below it hides the only trace the guard ever existed. `@ns.doc(...)` routinely wraps over
four lines, and stopping at its continuation drops the `@ns.route(...)` above it — the registration itself
(learning-harness Req 10.2)."""
first = index
cursor = index - 1
while cursor >= 0:
stripped = lines[cursor].strip()
if not stripped:
break
if _indent(lines[cursor]) == indent and stripped.startswith("@"):
first = cursor
cursor -= 1
continue
if _indent(lines[cursor]) == indent and stripped.startswith("#"):
if _COMMENTED_CODE.match(stripped):
first = cursor # `#@token_required` above the handler is the removed guard, not prose
cursor -= 1
continue
anchor = _decorator_anchor(lines, cursor, indent)
if anchor is None:
break
first = anchor
cursor = anchor - 1
return first
def _decorator_anchor(lines: Sequence[str], cursor: int, indent: int) -> int | None:
"""The `@` line of the multi-line decorator whose last line is ``cursor``, or None when ``cursor`` is not one."""
for start in range(cursor, -1, -1):
stripped = lines[start].strip()
if _indent(lines[start]) == indent and stripped.startswith("@"):
whole = " ".join(lines[position].strip() for position in range(start, cursor + 1))
opened = " ".join(lines[position].strip() for position in range(start, cursor))
balanced = whole.count("(") == whole.count(")") and opened.count("(") > opened.count(")")
return start if balanced else None
if stripped and _indent(lines[start]) <= indent:
return None
return None
def extract_handler_context(source: str, function_name: str, language: str = "c", *, line: int | None = None) -> str:
"""The named handler with everything that says it is reachable: its own decorators, the class its framework
registers (a `Resource`/`MethodView` method carries its route on the class, not on the function), and every
statement outside it that names the function as an identifier — `app.add_url_rule(..., view_func=fn)`,
`router.get('/x', fn)`, `path('x', fn)`, `app.use(fn)`. Comment and string mentions never count (the masked text
is searched). Pass ``line`` when the name repeats in the file: `def get` appears once per resource class, so the
name alone cannot say which handler a row labels. One excerpt per side, so guards living in decorators, routers
and middleware are visible to the teacher and the scorer (authorization-obligations Req 7.1, learning-harness
Req 10.2)."""
lines = source.splitlines()
start = _start_line_of(source, body) - 1
end = start + len(body.splitlines())
if line is not None:
bounds = innermost_function_bounds(source, int(line), language)
if bounds is None:
raise RecipeError(f"line {line} is not inside a function")
start, end = bounds[0] - 1, bounds[1]
# function_bounds_at anchors on the declarator; the decorators above it are part of the handler
start = _decorator_start(lines, start, _indent(lines[start]))
body = "\n".join(lines[start:end]) + "\n"
else:
body = extract_function(source, function_name, language)
start = _start_line_of(source, body) - 1
end = start + len(body.splitlines())
header = _python_class_header(lines, start) if language == "python" else ()
masked_lines = mask_comments_and_strings(source, language).splitlines()
mention = re.compile(rf"(?<![\w.]){re.escape(function_name)}(?!\w)")
declaration = re.compile(rf"^\s*(?:async\s+)?(?:def|function|sub)\s+{re.escape(function_name)}\b")
covered = set(range(start, end)) | set(header)
registrations = [
lines[index]
for index, masked in enumerate(masked_lines)
if not (start <= index < end) and index < len(lines) and mention.search(masked) and not declaration.match(masked)
if index not in covered and index < len(lines) and mention.search(masked) and not declaration.match(masked)
]
if header:
body = "\n".join(lines[index] for index in header) + "\n" + body
if not registrations:
return body
return body.rstrip("\n") + "\n\n" + "\n".join(line.strip() for line in registrations) + "\n"
def _indent(line: str) -> int:
return len(line) - len(line.lstrip())
def _python_class_header(lines: list[str], start: int) -> tuple[int, ...]:
"""Line indexes of the class declaration enclosing the handler at ``start``, with its decorators.
Flask-RESTX, Django REST framework and Flask's `MethodView` register the *class*: `@ns.route('/profile')` sits
above `class UserProfile(Resource)`, and the method below it is the handler. An excerpt that starts at `def get`
is both unreachable and unparsable (a bare indented block), so the class header travels with it."""
if start >= len(lines) or _indent(lines[start]) == 0:
return ()
indent = _indent(lines[start])
for index in range(start - 1, -1, -1):
match = _CLASS_RE.match(lines[index])
if match is None or len(match.group(1)) >= indent:
continue
return tuple(range(_decorator_start(lines, index, len(match.group(1))), index + 1))
return ()
def innermost_function_bounds(source: str, line: int, language: str = "c") -> tuple[int, int] | None:
"""1-based inclusive bounds of the *innermost* function containing ``line``.
`function_bounds_at` returns the outermost top-level brace group, which is right for C and wrong for the JavaScript
and TypeScript the absence corpus is made of: a handler declared inside a React component or a route module comes
back as the whole 6000-line component, and the excerpt starts mid-parameter-list. Only the handler-context mode
uses this, so hunk and enclosing rows keep the bounds they were harvested with."""
if language == "python":
return function_bounds_at(source, line, language)
masked = mask_comments_and_strings(source, language)
offsets = _line_offsets(source)
if line < 1 or line > len(offsets):
return None
target = offsets[line - 1]
stack: list[int] = []
best: tuple[int, int] | None = None
for pos, ch in enumerate(masked):
if ch == "{":
stack.append(pos)
elif ch == "}" and stack:
open_at = stack.pop()
if pos < target:
continue
start = _declarator_start(source, masked, open_at)
if not (start <= target <= pos):
continue # the declarator line counts: `const f = async () => {` opens its brace after the anchor
head = masked[start:open_at]
if "(" not in head or ")" not in head or _CONTROL_HEAD.match(head.strip()):
continue # an object literal, a bare block, or `if (...) {` — not a callable
if head.count("{") != head.count("}"):
continue # the declarator walk crossed an opening brace: this is a block inside the function, not the function
span = (_line_of(offsets, start), _line_of(offsets, pos))
if best is None or span[0] > best[0]:
best = span
return best
def extract_registration_context(source: str, function_name: str, language: str = "c") -> str:
"""The part of a *separate* document that registers ``function_name``, or "" when it names it nowhere.
Some frameworks keep the route table out of the handler file: connexion registers by `operationId` in an OpenAPI
document, an Express application registers in a router module. That statement carries the guard — the `security`
block, the authorization middleware — so an absence row harvested without it cannot be judged at all. Comment and
string mentions never count. Indented documents return the enclosing block with its ancestor headings; brace
documents return the whole enclosing call, which is where the middleware list lives."""
masked_lines = mask_comments_and_strings(source, language).splitlines()
lines = source.splitlines()
mention = re.compile(rf"(?<![\w.])(?:\w+\.)*{re.escape(function_name)}(?!\w)")
hits = [index for index, masked in enumerate(masked_lines) if mention.search(masked)]
if not hits:
return ""
if language in _INDENTED:
return _indented_block(lines, hits[0])
return _enclosing_call(source, masked_lines, lines, hits[0], function_name)
def _indented_block(lines: list[str], hit: int) -> str:
"""The innermost block containing ``hit``, prefixed by the heading line of every ancestor above it."""
indent = _indent(lines[hit])
ancestors: list[int] = []
current = indent
for index in range(hit - 1, -1, -1):
if not lines[index].strip():
continue
if _indent(lines[index]) < current:
ancestors.append(index)
current = _indent(lines[index])
if current == 0:
break
if not ancestors:
return lines[hit] + "\n"
inner = ancestors[0]
end = inner + 1
while end < len(lines) and (not lines[end].strip() or _indent(lines[end]) > _indent(lines[inner])):
end += 1
body = [*reversed(ancestors[1:]), *range(inner, end)]
return "\n".join(lines[index] for index in body).rstrip("\n") + "\n"
def _enclosing_call(source: str, masked_lines: list[str], lines: list[str], hit: int, function_name: str) -> str:
"""The whole call statement around the mention: `webRouter.post(` … `)` spans four lines and three of them are the guard."""
masked = "\n".join(masked_lines)
offsets = _line_offsets(masked + "\n")
at = masked.find(function_name, offsets[hit])
if at < 0:
return lines[hit] + "\n"
depth = 0
open_at = -1
for pos in range(at - 1, -1, -1):
ch = masked[pos]
if ch == ")":
depth += 1
elif ch == "(":
if depth == 0:
open_at = pos
break
depth -= 1
if open_at < 0:
return lines[hit] + "\n"
depth = 0
close_at = len(masked) - 1
for pos in range(open_at, len(masked)):
if masked[pos] == "(":
depth += 1
elif masked[pos] == ")":
depth -= 1
if depth == 0:
close_at = pos
break
start = _line_of(offsets, open_at) - 1
end = _line_of(offsets, close_at)
return "\n".join(lines[start:end]).rstrip("\n") + "\n"
def extract_line_range(source: str, start: int, end: int) -> str:
"""Inclusive 1-based line range."""
lines = source.splitlines(keepends=True)
@@ -371,7 +572,7 @@ def _python_bounds_at(lines: list[str], line: int) -> tuple[int, int] | None:
# --- recipes and IO ----------------------------------------------------------
def provenance_header(recipe: dict[str, Any], *, side: str, sha: str, upstream_start: int | None = None) -> str:
def provenance_header(recipe: dict[str, Any], *, side: str, sha: str, upstream_start: int | None = None, relpath: str | None = None) -> str:
lines = [
f"Provenance: {recipe.get('repo', '')} {recipe.get('function', '')} ({side}).",
f"repo: {recipe.get('repo', '')}",
@@ -387,7 +588,9 @@ def provenance_header(recipe: dict[str, Any], *, side: str, sha: str, upstream_s
]
if upstream_start is not None:
lines.append(f"upstream_start: {upstream_start}") # 1-based line of the excerpt body in the upstream file
language = language_for(recipe)
if relpath is not None:
lines[8] = f"relpath: {relpath}" # a context document names itself, not the handler file
language = language_for(recipe) if relpath is None else _SUFFIX_LANGUAGE.get(Path(relpath).suffix.lower(), "c")
if language in _HASH_COMMENT:
return "\n".join(f"# {line}" for line in lines) + "\n"
if language in {"cpp", "java", "javascript", "typescript"}:
@@ -414,6 +617,10 @@ def validate_recipe(recipe: dict[str, Any], *, require_license: bool = True) ->
raise RecipeError(f"{name}: enclosing mode needs an integer line")
if require_license and not recipe.get("license"):
raise RecipeError(f"{name}: license required")
for relpath in context_relpaths(recipe):
suffix = Path(relpath).suffix.lower()
if _SUFFIX_LANGUAGE.get(suffix) not in _CONTEXT_LANGUAGES:
raise RecipeError(f"{name}: context document {relpath} has no supported registration reader")
def github_raw_url(repo: str, sha: str, path: str) -> str:
@@ -427,6 +634,22 @@ def excerpt_paths(recipe: dict[str, Any], root: Path) -> tuple[Path, Path]:
return folder / f"vuln{ext}", folder / f"fixed{ext}"
def context_relpaths(recipe: dict[str, Any]) -> tuple[str, ...]:
"""Repo-relative documents that register the handler, fetched at the same two commits as the excerpt."""
value = recipe.get("context") or ()
if isinstance(value, str):
return (value,)
return tuple(str(item) for item in value)
def context_paths(recipe: dict[str, Any], root: Path) -> tuple[tuple[str, Path, Path], ...]:
"""`(relpath, vuln document, fixed document)` per context document. Each side keeps its own copy: the route table
is part of the change often enough (a guard added to the router, not to the handler) that sharing one would hide
the fix."""
folder = root / str(recipe["name"]) / "context"
return tuple((relpath, folder / "vuln" / relpath, folder / "fixed" / relpath) for relpath in context_relpaths(recipe))
_TOKEN_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
(re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----", re.DOTALL), "***REDACTED***"),
(re.compile(r"(Bearer\s+)[A-Za-z0-9._\-]{12,}"), r"\1***REDACTED***"),
@@ -452,9 +675,18 @@ def redact(body: str) -> str:
return body
def write_excerpt(path: Path, recipe: dict[str, Any], *, side: str, sha: str, body: str, upstream_start: int | None = None) -> None:
def write_excerpt(
path: Path,
recipe: dict[str, Any],
*,
side: str,
sha: str,
body: str,
upstream_start: int | None = None,
relpath: str | None = None,
) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
header = provenance_header(recipe, side=side, sha=sha, upstream_start=upstream_start)
header = provenance_header(recipe, side=side, sha=sha, upstream_start=upstream_start, relpath=relpath)
path.write_text(header + "\n" + redact(body), encoding="utf-8")
@@ -478,12 +710,30 @@ def extract_pair_with_lines(recipe: dict[str, Any], parent_src: str, fix_src: st
raise RecipeError(f"{recipe.get('name')}: line {line}/{fix_line} is not inside a function")
return (extract_line_range(parent_src, *parent_bounds), parent_bounds[0]), (extract_line_range(fix_src, *fix_bounds), fix_bounds[0])
function = str(recipe["function"])
extract = extract_handler_context if mode == "handler_context" else extract_function
parent_text = extract(parent_src, function, language)
fix_text = extract(fix_src, function, language)
if mode == "handler_context":
parent_text, fix_text = _handler_context_pair(recipe, parent_src, fix_src, language)
else:
parent_text, fix_text = extract_function(parent_src, function, language), extract_function(fix_src, function, language)
return (parent_text, _start_line_of(parent_src, parent_text)), (fix_text, _start_line_of(fix_src, fix_text))
def _handler_context_pair(recipe: dict[str, Any], parent_src: str, fix_src: str, language: str) -> tuple[str, str]:
"""Both sides with their registration. ``line``/``fix_line`` anchor when the name repeats or when the fixed twin is
a different, correctly guarded handler from the same snapshot (a Real-Vuln-Benchmark trap): then ``fix_function``
names what the fixed side actually contains."""
line = recipe.get("line")
fix_line = recipe.get("fix_line", line)
return (
extract_handler_context(parent_src, str(recipe["function"]), language, line=int(line) if isinstance(line, int) else None),
extract_handler_context(
fix_src,
str(recipe.get("fix_function") or recipe["function"]),
language,
line=int(fix_line) if isinstance(fix_line, int) else None,
),
)
def _start_line_of(source: str, excerpt: str) -> int:
first = excerpt.splitlines()[0] if excerpt.strip() else ""
for index, line in enumerate(source.splitlines(), start=1):
@@ -534,24 +784,45 @@ def extract_pair(recipe: dict[str, Any], parent_src: str, fix_src: str) -> tuple
raise RecipeError(f"{recipe.get('name')}: line {line}/{fix_line} is not inside a function")
return extract_line_range(parent_src, *parent_bounds), extract_line_range(fix_src, *fix_bounds)
function = str(recipe["function"])
extract = extract_handler_context if mode == "handler_context" else extract_function
return extract(parent_src, function, language), extract(fix_src, function, language)
if mode == "handler_context":
return _handler_context_pair(recipe, parent_src, fix_src, language)
return extract_function(parent_src, function, language), extract_function(fix_src, function, language)
def harvest_recipe(recipe: dict[str, Any], root: Path, *, require_license: bool = True) -> tuple[Path, Path]:
"""Fetch every blob the recipe names, extract, and only then write.
Every fetch happens before the first write on purpose: a re-harvest that half succeeds would leave a reviewed
excerpt truncated and its context document stale, and nothing downstream could tell that apart from a real
upstream change (learning-harness Req 10.2)."""
validate_recipe(recipe, require_license=require_license)
if str(recipe.get("host", "github")) != "github":
raise RecipeError(f"{recipe.get('name')}: live fetch currently implemented for host=github only")
parent_src = fetch_url(github_raw_url(str(recipe["repo"]), str(recipe["parent"]), str(recipe["path"])))
repo, parent, commit = str(recipe["repo"]), str(recipe["parent"]), str(recipe["commit"])
fix_repo = str(recipe.get("fix_repo", repo))
parent_src = fetch_url(github_raw_url(repo, parent, str(recipe["path"])))
# fix_path lets a pair take its fixed side from another file of the same snapshot
# (Real-Vuln-Benchmark false-positive traps are the fixed twins of vulnerable findings).
fix_src = fetch_url(
github_raw_url(str(recipe.get("fix_repo", recipe["repo"])), str(recipe["commit"]), str(recipe.get("fix_path", recipe["path"])))
)
fix_src = fetch_url(github_raw_url(fix_repo, commit, str(recipe.get("fix_path", recipe["path"]))))
function = str(recipe.get("function", ""))
fix_function = str(recipe.get("fix_function") or function)
documents: list[tuple[Path, str, str, str, str]] = []
for relpath, vuln_doc, fixed_doc in context_paths(recipe, root):
language = _SUFFIX_LANGUAGE.get(Path(relpath).suffix.lower(), "c")
for path, side, sha, wanted, source in (
(vuln_doc, "vuln", parent, function, fetch_url(github_raw_url(repo, parent, relpath))),
(fixed_doc, "fixed", commit, fix_function, fetch_url(github_raw_url(fix_repo, commit, relpath))),
):
stanza = extract_registration_context(source, wanted, language)
if not stanza.strip():
raise RecipeError(f"{recipe.get('name')}: context document {relpath} never names {wanted}")
documents.append((path, side, sha, stanza, relpath))
(parent_fn, parent_start), (fix_fn, fix_start) = extract_pair_with_lines(recipe, parent_src, fix_src)
vuln_path, fixed_path = excerpt_paths(recipe, root)
write_excerpt(vuln_path, recipe, side="vuln", sha=str(recipe["parent"]), body=parent_fn, upstream_start=parent_start)
write_excerpt(fixed_path, recipe, side="fixed", sha=str(recipe["commit"]), body=fix_fn, upstream_start=fix_start)
write_excerpt(vuln_path, recipe, side="vuln", sha=parent, body=parent_fn, upstream_start=parent_start)
write_excerpt(fixed_path, recipe, side="fixed", sha=commit, body=fix_fn, upstream_start=fix_start)
for path, side, sha, stanza, relpath in documents:
write_excerpt(path, recipe, side=side, sha=sha, body=stanza, relpath=relpath)
return vuln_path, fixed_path
@@ -599,6 +870,7 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument("--fetch", action="store_true", help="HTTP-fetch and write excerpts; default is dry-run validate")
parser.add_argument("--force", action="store_true", help="overwrite existing excerpts")
parser.add_argument("--out", type=Path, default=None, help="excerpt root (default: <slice>/)")
parser.add_argument("--report", type=Path, default=None, help="write a JSON row per recipe with its status and reason")
args = parser.parse_args(argv)
slice_root = ROOT / args.slice
recipes_path = args.recipes or slice_root / "recipes.toml"
@@ -619,28 +891,38 @@ def main(argv: list[str] | None = None) -> int:
print("pass --name RECIPE, --prefix PREFIX, or --all", file=sys.stderr)
return 2
failures = 0
report: list[dict[str, str]] = []
for recipe in selected:
name = str(recipe["name"])
try:
validate_recipe(recipe)
except RecipeError as exc:
print(str(exc), file=sys.stderr)
report.append({"name": name, "status": "failed", "reason": str(exc)})
failures += 1
continue
if not args.fetch:
print(f"ok {name} parent={recipe['parent']} commit={recipe['commit']} mode={recipe.get('mode', 'name')}")
report.append({"name": name, "status": "validated", "reason": ""})
continue
vuln_path, fixed_path = excerpt_paths(recipe, out)
if vuln_path.is_file() and fixed_path.is_file() and not args.force:
print(f"skip {name} (exists)")
report.append({"name": name, "status": "skipped", "reason": "excerpt already present"})
continue
try:
harvest_recipe(recipe, out)
except (RecipeError, OSError) as exc:
# The excerpt on disk is the reviewed one; a fetch that failed leaves it exactly as it was and says why.
print(f"{name}: {exc}", file=sys.stderr)
report.append({"name": name, "status": "failed", "reason": str(exc)})
failures += 1
continue
print(f"wrote {name} -> {vuln_path.parent}")
report.append({"name": name, "status": "written", "reason": ""})
if args.report is not None:
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
return 1 if failures else 0
+14
View File
@@ -3272,6 +3272,7 @@ class = "broken_access_control"
path = "server/api/v1/api_v1.py"
function = "delete"
mechanism = "missing_auth_guard"
family = "access_control"
evidence = "BFLA (Broken Function Level Authorization) on the delete-user API endpoint. The @token_required decorator is commented out (lines 221-222), so DELETE /api/v1/delete-user/<user_id> can be called by anyone without authentication to delete any"
[[pair]]
@@ -3301,6 +3302,7 @@ class = "broken_access_control"
path = "server/api/v1/api_v1.py"
function = "get"
mechanism = "missing_auth_guard"
family = "access_control"
evidence = "BOLA (Broken Object Level Authorization) on the API profile endpoint. The @token_required decorator is commented out (line 152-153), so /api/v1/profile?user_id=X returns any user's profile (username, email, country, role, permissions, team)"
[[pair]]
@@ -3353,12 +3355,18 @@ provenance = "human"
split = "holdout"
review_tier = "seeded"
[[pair.context]]
relpath = "openapi_specs/openapi3.yml"
vuln = "vampi-books-get-by-title/context/vuln/openapi_specs/openapi3.yml"
fixed = "vampi-books-get-by-title/context/fixed/openapi_specs/openapi3.yml"
[[pair.expected]]
cwe = "CWE-639"
class = "broken_access_control"
path = "api_views/books.py"
function = "get_by_title"
mechanism = "missing_auth_guard"
family = "access_control"
evidence = "Broken Object Level Authorization (BOLA). When vuln=1, get_by_title queries the book by title only (Book.query.filter_by(book_title=str(book_title)).first()) without verifying the authenticated user is the owner. Any authenticated user can "
[[pair]]
@@ -3411,12 +3419,18 @@ provenance = "human"
split = "holdout"
review_tier = "seeded"
[[pair.context]]
relpath = "openapi_specs/openapi3.yml"
vuln = "vampi-users-update-password/context/vuln/openapi_specs/openapi3.yml"
fixed = "vampi-users-update-password/context/fixed/openapi_specs/openapi3.yml"
[[pair.expected]]
cwe = "CWE-639"
class = "broken_access_control"
path = "api_views/users.py"
function = "update_password"
mechanism = "missing_auth_guard"
family = "access_control"
evidence = "Unauthorized password change (IDOR). When vuln=1, the update_password function uses the username from the URL path parameter to look up the user whose password to change, instead of using the authenticated user from the JWT token (resp['sub"
[[pair]]
+13 -4
View File
@@ -3208,7 +3208,7 @@ parent = "c4db46f72586c4ecb65859051ea037018dbf0e5e"
commit = "c4db46f72586c4ecb65859051ea037018dbf0e5e"
path = "server/api/v1/api_v1.py"
fix_path = "server/api/v1/api_v1.py"
mode = "enclosing"
mode = "handler_context"
line = 223
fix_line = 123
function = "delete"
@@ -3229,6 +3229,8 @@ trap_id = "threatbyte-fp-004"
acceptable_cwes = ["CWE-862", "CWE-306", "CWE-284", "CWE-287"]
evidence = "BFLA (Broken Function Level Authorization) on the delete-user API endpoint. The @token_required decorator is commented out (lines 221-222), so DELETE /api/v1/delete-user/<user_id> can be called by anyone without authentication to delete any"
commit_url = "https://github.com/anotherik/ThreatByte/blob/c4db46f72586c4ecb65859051ea037018dbf0e5e/server/api/v1/api_v1.py#L223"
family = "access_control"
fix_function = "post"
split = "train"
[[recipe]]
@@ -3239,7 +3241,7 @@ parent = "c4db46f72586c4ecb65859051ea037018dbf0e5e"
commit = "c4db46f72586c4ecb65859051ea037018dbf0e5e"
path = "server/api/v1/api_v1.py"
fix_path = "server/api/v1/api_v1.py"
mode = "enclosing"
mode = "handler_context"
line = 154
fix_line = 123
function = "get"
@@ -3260,6 +3262,8 @@ trap_id = "threatbyte-fp-004"
acceptable_cwes = ["CWE-639", "CWE-284", "CWE-862", "CWE-863"]
evidence = "BOLA (Broken Object Level Authorization) on the API profile endpoint. The @token_required decorator is commented out (line 152-153), so /api/v1/profile?user_id=X returns any user's profile (username, email, country, role, permissions, team)"
commit_url = "https://github.com/anotherik/ThreatByte/blob/c4db46f72586c4ecb65859051ea037018dbf0e5e/server/api/v1/api_v1.py#L154"
family = "access_control"
fix_function = "post"
split = "holdout"
[[recipe]]
@@ -3301,7 +3305,7 @@ parent = "1713b54b601ad29582581eeda4b31fceb1319874"
commit = "1713b54b601ad29582581eeda4b31fceb1319874"
path = "api_views/books.py"
fix_path = "api_views/books.py"
mode = "enclosing"
mode = "handler_context"
line = 51
fix_line = 62
function = "get_by_title"
@@ -3322,6 +3326,8 @@ trap_id = "vampi-fp-003"
acceptable_cwes = ["CWE-639", "CWE-284", "CWE-285", "CWE-862"]
evidence = "Broken Object Level Authorization (BOLA). When vuln=1, get_by_title queries the book by title only (Book.query.filter_by(book_title=str(book_title)).first()) without verifying the authenticated user is the owner. Any authenticated user can "
commit_url = "https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/api_views/books.py#L51"
family = "access_control"
context = ["openapi_specs/openapi3.yml"]
split = "holdout"
[[recipe]]
@@ -3363,7 +3369,7 @@ parent = "1713b54b601ad29582581eeda4b31fceb1319874"
commit = "1713b54b601ad29582581eeda4b31fceb1319874"
path = "api_views/users.py"
fix_path = "api_views/users.py"
mode = "enclosing"
mode = "handler_context"
line = 187
fix_line = 55
function = "update_password"
@@ -3384,6 +3390,9 @@ trap_id = "vampi-fp-001"
acceptable_cwes = ["CWE-639", "CWE-284", "CWE-285", "CWE-862"]
evidence = "Unauthorized password change (IDOR). When vuln=1, the update_password function uses the username from the URL path parameter to look up the user whose password to change, instead of using the authenticated user from the JWT token (resp['sub"
commit_url = "https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/api_views/users.py#L187"
family = "access_control"
fix_function = "register_user"
context = ["openapi_specs/openapi3.yml"]
split = "holdout"
[[recipe]]
@@ -9,39 +9,47 @@
# relpath: server/api/v1/api_v1.py
# provenance: human
# mechanism: missing_auth_guard
# upstream_start: 103
def post(self):
"""
Handles user login.
"""
data = request.get_json() # Get data from request body
username = data.get('username')
password = data.get('password')
if not username or not password:
return {'error': 'Please provide username and password'}, 400
with get_db_connection() as conn:
cursor = conn.cursor()
query = "SELECT * FROM users WHERE username = ? OR email = ?"
cursor.execute(query, (username, username))
user = cursor.fetchone()
if user and user['password'] == custom_hash(password):
# Generate JWT
token = jwt.encode({
'username': username,
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24) # Token expires in 24 hours
}, Config.SECRET_KEY, algorithm='HS256')
# Update last_login timestamp
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("UPDATE users SET last_login = ? WHERE username = ?", (datetime.datetime.now(), username))
conn.commit()
return {'message': 'Login successful', 'token': token}, 200
else:
return {'error': 'Invalid username or password'}, 401
@ns.route('/login')
class UserLogin(Resource):
@ns.expect(login_model)
@ns.response(200, 'Login successful', login_response_model)
@ns.response(400, 'Please provide username and password')
@ns.response(401, 'Invalid username or password')
@ns.response(405, 'Method not allowed')
def post(self):
"""
Handles user login.
"""
data = request.get_json() # Get data from request body
username = data.get('username')
password = data.get('password')
if not username or not password:
return {'error': 'Please provide username and password'}, 400
with get_db_connection() as conn:
cursor = conn.cursor()
query = "SELECT * FROM users WHERE username = ? OR email = ?"
cursor.execute(query, (username, username))
user = cursor.fetchone()
if user and user['password'] == custom_hash(password):
# Generate JWT
token = jwt.encode({
'username': username,
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24) # Token expires in 24 hours
}, Config.SECRET_KEY, algorithm='HS256')
# Update last_login timestamp
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("UPDATE users SET last_login = ? WHERE username = ?", (datetime.datetime.now(), username))
conn.commit()
return {'message': 'Login successful', 'token': token}, 200
else:
return {'error': 'Invalid username or password'}, 401
@@ -9,20 +9,29 @@
# relpath: server/api/v1/api_v1.py
# provenance: human
# mechanism: missing_auth_guard
# upstream_start: 215
def delete(self, user_id):
"""
Delete a user from the application.
"""
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if user:
cursor.execute("DELETE FROM users WHERE id = ?", (user_id,))
conn.commit()
response = make_response(jsonify({'message': 'User deleted successfully'}), 200)
return response
else:
response = make_response(jsonify({'error': 'User not found'}), 404)
return response
@ns.route('/delete-user/<int:user_id>')
@ns.doc(description='Delete a user from the database without proper authorization checks. '
'This endpoint represents a Broken Function Level Authorization vulnerability.',
responses={200: ('User successfully deleted', delete_model),
404: ('User not found', error_model)})
class UserDelete(Resource):
#@token_required
#def delete(self, current_user, user_id):
def delete(self, user_id):
"""
Delete a user from the application.
"""
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if user:
cursor.execute("DELETE FROM users WHERE id = ?", (user_id,))
conn.commit()
response = make_response(jsonify({'message': 'User deleted successfully'}), 200)
return response
else:
response = make_response(jsonify({'error': 'User not found'}), 404)
return response
@@ -9,39 +9,47 @@
# relpath: server/api/v1/api_v1.py
# provenance: human
# mechanism: missing_auth_guard
# upstream_start: 103
def post(self):
"""
Handles user login.
"""
data = request.get_json() # Get data from request body
username = data.get('username')
password = data.get('password')
if not username or not password:
return {'error': 'Please provide username and password'}, 400
with get_db_connection() as conn:
cursor = conn.cursor()
query = "SELECT * FROM users WHERE username = ? OR email = ?"
cursor.execute(query, (username, username))
user = cursor.fetchone()
if user and user['password'] == custom_hash(password):
# Generate JWT
token = jwt.encode({
'username': username,
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24) # Token expires in 24 hours
}, Config.SECRET_KEY, algorithm='HS256')
# Update last_login timestamp
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("UPDATE users SET last_login = ? WHERE username = ?", (datetime.datetime.now(), username))
conn.commit()
return {'message': 'Login successful', 'token': token}, 200
else:
return {'error': 'Invalid username or password'}, 401
@ns.route('/login')
class UserLogin(Resource):
@ns.expect(login_model)
@ns.response(200, 'Login successful', login_response_model)
@ns.response(400, 'Please provide username and password')
@ns.response(401, 'Invalid username or password')
@ns.response(405, 'Method not allowed')
def post(self):
"""
Handles user login.
"""
data = request.get_json() # Get data from request body
username = data.get('username')
password = data.get('password')
if not username or not password:
return {'error': 'Please provide username and password'}, 400
with get_db_connection() as conn:
cursor = conn.cursor()
query = "SELECT * FROM users WHERE username = ? OR email = ?"
cursor.execute(query, (username, username))
user = cursor.fetchone()
if user and user['password'] == custom_hash(password):
# Generate JWT
token = jwt.encode({
'username': username,
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24) # Token expires in 24 hours
}, Config.SECRET_KEY, algorithm='HS256')
# Update last_login timestamp
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("UPDATE users SET last_login = ? WHERE username = ?", (datetime.datetime.now(), username))
conn.commit()
return {'message': 'Login successful', 'token': token}, 200
else:
return {'error': 'Invalid username or password'}, 401
@@ -9,21 +9,30 @@
# relpath: server/api/v1/api_v1.py
# provenance: human
# mechanism: missing_auth_guard
# upstream_start: 146
def get(self):
"""
Retrieves the profile information of a user based on the provided user ID.
"""
args = profile_query.parse_args()
user_id = args.get('user_id')
with get_db_connection() as conn:
cursor = conn.cursor()
query = "SELECT username, email, country, role, permissions, team FROM users WHERE id = ?"
cursor.execute(query, (user_id,))
user_profile = cursor.fetchone()
if user_profile:
return dict(user_profile), 200
else:
return {'error': 'User profile not found'}, 404
@ns.route('/profile')
class UserProfile(Resource):
@ns.expect(profile_query)
@ns.response(200, 'Success', user_profile_model)
@ns.response(400, 'User ID is required')
@ns.response(404, 'User profile not found')
#@token_required
#def get(self, current_user):
def get(self):
"""
Retrieves the profile information of a user based on the provided user ID.
"""
args = profile_query.parse_args()
user_id = args.get('user_id')
with get_db_connection() as conn:
cursor = conn.cursor()
query = "SELECT username, email, country, role, permissions, team FROM users WHERE id = ?"
cursor.execute(query, (user_id,))
user_profile = cursor.fetchone()
if user_profile:
return dict(user_profile), 200
else:
return {'error': 'User profile not found'}, 404
@@ -0,0 +1,79 @@
# Provenance: erev0s/VAmPI get_by_title (fixed).
# repo: erev0s/VAmPI
# commit: 1713b54b601ad29582581eeda4b31fceb1319874
# parent: 1713b54b601ad29582581eeda4b31fceb1319874
# commit_url: https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/api_views/books.py#L51
# cve:
# license: MIT
# function: get_by_title
# relpath: openapi_specs/openapi3.yml
# provenance: human
# mechanism: missing_auth_guard
paths:
/books/v1/{book_title}:
get:
security:
- bearerAuth: [ ]
tags:
- books
summary: Retrieves book by title along with secret
description: >-
Retrieves book by title along with secret. Only the owner may retrieve
it
operationId: api_views.books.get_by_title
parameters:
- name: book_title
in: path
description: retrieve book data
required: true
schema:
type: string
example: 'bookTitle77'
responses:
'200':
description: Successfully retrieve book info
content:
application/json:
schema:
type: array
items:
type: object
properties:
book_title:
type: string
example: 'bookTitle77'
owner:
type: string
example: 'name1'
secret:
type: string
example: 'secret for bookTitle77'
'401':
description: User not authorized
content:
application/json:
schema:
type: object
properties:
status:
type: string
enum: ['fail']
example: 'fail'
message:
type: string
example: 'Invalid token. Please log in again.'
'404':
description: Book not found
content:
application/json:
schema:
type: object
properties:
status:
type: string
enum: ['fail']
example: 'fail'
message:
type: string
example: 'Book not found!'
@@ -0,0 +1,79 @@
# Provenance: erev0s/VAmPI get_by_title (vuln).
# repo: erev0s/VAmPI
# commit: 1713b54b601ad29582581eeda4b31fceb1319874
# parent: 1713b54b601ad29582581eeda4b31fceb1319874
# commit_url: https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/api_views/books.py#L51
# cve:
# license: MIT
# function: get_by_title
# relpath: openapi_specs/openapi3.yml
# provenance: human
# mechanism: missing_auth_guard
paths:
/books/v1/{book_title}:
get:
security:
- bearerAuth: [ ]
tags:
- books
summary: Retrieves book by title along with secret
description: >-
Retrieves book by title along with secret. Only the owner may retrieve
it
operationId: api_views.books.get_by_title
parameters:
- name: book_title
in: path
description: retrieve book data
required: true
schema:
type: string
example: 'bookTitle77'
responses:
'200':
description: Successfully retrieve book info
content:
application/json:
schema:
type: array
items:
type: object
properties:
book_title:
type: string
example: 'bookTitle77'
owner:
type: string
example: 'name1'
secret:
type: string
example: 'secret for bookTitle77'
'401':
description: User not authorized
content:
application/json:
schema:
type: object
properties:
status:
type: string
enum: ['fail']
example: 'fail'
message:
type: string
example: 'Invalid token. Please log in again.'
'404':
description: Book not found
content:
application/json:
schema:
type: object
properties:
status:
type: string
enum: ['fail']
example: 'fail'
message:
type: string
example: 'Book not found!'
@@ -9,6 +9,7 @@
# relpath: api_views/books.py
# provenance: human
# mechanism: missing_auth_guard
# upstream_start: 45
def get_by_title(book_title):
resp = token_validator(request.headers.get('Authorization'))
@@ -9,6 +9,7 @@
# relpath: api_views/books.py
# provenance: human
# mechanism: missing_auth_guard
# upstream_start: 45
def get_by_title(book_title):
resp = token_validator(request.headers.get('Authorization'))
@@ -0,0 +1,55 @@
# Provenance: erev0s/VAmPI update_password (fixed).
# repo: erev0s/VAmPI
# commit: 1713b54b601ad29582581eeda4b31fceb1319874
# parent: 1713b54b601ad29582581eeda4b31fceb1319874
# commit_url: https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/api_views/users.py#L187
# cve:
# license: MIT
# function: update_password
# relpath: openapi_specs/openapi3.yml
# provenance: human
# mechanism: missing_auth_guard
paths:
/users/v1/register:
post:
tags:
- users
summary: Register new user
description: Register new user
operationId: api_views.users.register_user
requestBody:
description: Username of the user
content:
application/json:
schema:
type: object
properties:
username:
type: string
example: 'name1'
password:
type: string
example: 'pass1'
email:
type: string
example: '[email protected]'
required: true
responses:
'200':
description: Successfully created user
content:
application/json:
schema:
type: object
properties:
message:
type: string
example: 'Successfully registered. Login to receive an auth token.'
status:
type: string
enum: ['success', 'fail']
example: 'success'
'400':
description: Invalid request
content: {}
@@ -0,0 +1,73 @@
# Provenance: erev0s/VAmPI update_password (vuln).
# repo: erev0s/VAmPI
# commit: 1713b54b601ad29582581eeda4b31fceb1319874
# parent: 1713b54b601ad29582581eeda4b31fceb1319874
# commit_url: https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/api_views/users.py#L187
# cve:
# license: MIT
# function: update_password
# relpath: openapi_specs/openapi3.yml
# provenance: human
# mechanism: missing_auth_guard
paths:
/users/v1/{username}/password:
put:
security:
- bearerAuth: [ ]
tags:
- users
summary: Update users password
description: Update users password
operationId: api_views.users.update_password
parameters:
- name: username
in: path
description: username to update password
required: true
schema:
type: string
example: 'name1'
requestBody:
description: field to update
content:
application/json:
schema:
type: object
properties:
password:
type: string
example: 'pass4'
required: true
responses:
'204':
description: Successfully updated users password
content: {}
'400':
description: Invalid request
content:
application/json:
schema:
type: object
properties:
status:
type: string
enum: ['fail']
example: 'fail'
message:
type: string
example: 'Malformed Data'
'401':
description: User not authorized
content:
application/json:
schema:
type: object
properties:
status:
type: string
enum: ['fail']
example: 'fail'
message:
type: string
example: 'Invalid token. Please log in again.'
@@ -9,6 +9,7 @@
# relpath: api_views/users.py
# provenance: human
# mechanism: missing_auth_guard
# upstream_start: 52
def register_user():
request_data = request.get_json()
@@ -9,6 +9,7 @@
# relpath: api_views/users.py
# provenance: human
# mechanism: missing_auth_guard
# upstream_start: 179
def update_password(username):
request_data = request.get_json()