mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-17 03:59:28 +00:00
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:
co-authored by
Claude Opus 5
parent
7a33756a7e
commit
c2d9c4ad01
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
+17
@@ -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
|
||||
)
|
||||
+17
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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
|
||||
|
||||
+79
@@ -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!'
|
||||
+79
@@ -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'))
|
||||
|
||||
+55
@@ -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: {}
|
||||
+73
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user