codejury 0.12.0__tar.gz → 0.13.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codejury-0.12.0 → codejury-0.13.2}/PKG-INFO +2 -1
- {codejury-0.12.0 → codejury-0.13.2}/codejury/cli.py +62 -5
- codejury-0.13.2/codejury/data/rules/SKILL.md +58 -0
- codejury-0.13.2/codejury/data/rules/business-logic.md +23 -0
- codejury-0.13.2/codejury/data/rules/code-injection.md +23 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/data/rules/command-injection.md +3 -2
- codejury-0.13.2/codejury/data/rules/cross-site-request-forgery.md +21 -0
- codejury-0.12.0/codejury/data/rules/xss.md → codejury-0.13.2/codejury/data/rules/cross-site-scripting.md +6 -5
- codejury-0.13.2/codejury/data/rules/hardcoded-secrets.md +21 -0
- codejury-0.13.2/codejury/data/rules/http-response-splitting.md +24 -0
- codejury-0.13.2/codejury/data/rules/improper-authentication.md +26 -0
- codejury-0.13.2/codejury/data/rules/information-exposure.md +34 -0
- codejury-0.13.2/codejury/data/rules/insecure-cryptography.md +23 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/data/rules/insecure-deserialization.md +2 -1
- codejury-0.13.2/codejury/data/rules/insecure-direct-object-reference.md +25 -0
- codejury-0.13.2/codejury/data/rules/insecure-transport.md +22 -0
- codejury-0.13.2/codejury/data/rules/jwt-validation.md +22 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/data/rules/mass-assignment.md +3 -2
- codejury-0.13.2/codejury/data/rules/missing-authorization.md +28 -0
- codejury-0.13.2/codejury/data/rules/open-redirect.md +24 -0
- codejury-0.13.2/codejury/data/rules/path-traversal.md +34 -0
- codejury-0.13.2/codejury/data/rules/race-condition.md +26 -0
- codejury-0.13.2/codejury/data/rules/replay-attack.md +23 -0
- codejury-0.12.0/codejury/data/rules/ssrf.md → codejury-0.13.2/codejury/data/rules/server-side-request-forgery.md +4 -3
- codejury-0.13.2/codejury/data/rules/server-side-template-injection.md +21 -0
- codejury-0.13.2/codejury/data/rules/session-fixation.md +23 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/data/rules/sql-injection.md +4 -3
- codejury-0.13.2/codejury/data/rules/xml-external-entity.md +23 -0
- codejury-0.13.2/codejury/diff/debate.py +162 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/diff/debate_prompts.py +26 -12
- {codejury-0.12.0 → codejury-0.13.2}/codejury/diff/engine.py +15 -1
- codejury-0.13.2/codejury/diff/findings_filter.py +61 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/diff/prompts.py +15 -1
- {codejury-0.12.0 → codejury-0.13.2}/codejury/diff/rules.py +16 -0
- codejury-0.13.2/codejury/infrastructure/json_parse.py +98 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/providers/anthropic.py +1 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/providers/factory.py +4 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/providers/litellm.py +1 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/providers/openai.py +1 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/providers/retry.py +16 -2
- {codejury-0.12.0 → codejury-0.13.2}/codejury.egg-info/PKG-INFO +2 -1
- {codejury-0.12.0 → codejury-0.13.2}/codejury.egg-info/SOURCES.txt +21 -7
- {codejury-0.12.0 → codejury-0.13.2}/codejury.egg-info/requires.txt +1 -0
- {codejury-0.12.0 → codejury-0.13.2}/pyproject.toml +2 -1
- codejury-0.13.2/tests/test_cli_audit.py +45 -0
- {codejury-0.12.0 → codejury-0.13.2}/tests/test_diff_debate.py +76 -1
- {codejury-0.12.0 → codejury-0.13.2}/tests/test_diff_engine.py +33 -3
- {codejury-0.12.0 → codejury-0.13.2}/tests/test_json_parse.py +16 -0
- {codejury-0.12.0 → codejury-0.13.2}/tests/test_retry_provider.py +28 -1
- {codejury-0.12.0 → codejury-0.13.2}/tests/test_rules.py +36 -3
- codejury-0.12.0/codejury/data/rules/SKILL.md +0 -41
- codejury-0.12.0/codejury/data/rules/authentication-jwt.md +0 -21
- codejury-0.12.0/codejury/data/rules/broken-access-control.md +0 -27
- codejury-0.12.0/codejury/data/rules/idor.md +0 -24
- codejury-0.12.0/codejury/data/rules/insecure-crypto.md +0 -22
- codejury-0.12.0/codejury/data/rules/path-traversal.md +0 -22
- codejury-0.12.0/codejury/data/rules/secrets.md +0 -20
- codejury-0.12.0/codejury/diff/debate.py +0 -104
- codejury-0.12.0/codejury/diff/findings_filter.py +0 -39
- codejury-0.12.0/codejury/infrastructure/json_parse.py +0 -62
- {codejury-0.12.0 → codejury-0.13.2}/LICENSE +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/README.md +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/__init__.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/analysis/__init__.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/analysis/repo_model.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/data/agent/full-review.md +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/data/agent/security-review-memory.md +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/data/entrypoints.yaml +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/diff/__init__.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/diff/report.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/domain/__init__.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/domain/finding.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/fullreview/__init__.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/fullreview/scaffold.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/infrastructure/__init__.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/providers/__init__.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/providers/base.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/providers/mock.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/providers/openai_format.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury/resources.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury.egg-info/dependency_links.txt +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury.egg-info/entry_points.txt +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/codejury.egg-info/top_level.txt +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/setup.cfg +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/tests/test_anthropic_provider.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/tests/test_diff_report.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/tests/test_full_review_scaffold.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/tests/test_litellm_provider.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/tests/test_openai_format.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/tests/test_openai_provider.py +0 -0
- {codejury-0.12.0 → codejury-0.13.2}/tests/test_repo_model.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codejury
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.2
|
|
4
4
|
Summary: AI code security review: an adversarial diff-audit engine and an agent-driven whole-repo review methodology, with security knowledge as rich rules
|
|
5
5
|
Author: AISecLabs
|
|
6
6
|
License-Expression: MIT
|
|
@@ -17,6 +17,7 @@ Requires-Python: >=3.12
|
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: pyyaml>=6.0
|
|
20
|
+
Requires-Dist: json-repair>=0.30
|
|
20
21
|
Provides-Extra: anthropic
|
|
21
22
|
Requires-Dist: anthropic>=0.40; extra == "anthropic"
|
|
22
23
|
Provides-Extra: openai
|
|
@@ -17,15 +17,25 @@ import argparse
|
|
|
17
17
|
import subprocess
|
|
18
18
|
import sys
|
|
19
19
|
|
|
20
|
+
import dataclasses
|
|
21
|
+
|
|
20
22
|
from codejury.diff.debate import AdversarialAuditRunner
|
|
21
23
|
from codejury.diff.engine import AuditRunner
|
|
22
24
|
from codejury.diff.findings_filter import FindingsFilter
|
|
23
25
|
from codejury.diff.report import gate, render
|
|
26
|
+
from codejury.diff.rules import allowed_categories, normalize_category
|
|
24
27
|
from codejury.domain.finding import Finding
|
|
28
|
+
|
|
29
|
+
# A diff larger than this is audited file-by-file so a big PR does not overflow
|
|
30
|
+
# the model's context and silently truncate the reply.
|
|
31
|
+
_MAX_DIFF_CHARS = 60_000
|
|
25
32
|
from codejury.fullreview.scaffold import scaffold
|
|
26
33
|
from codejury.providers.factory import (
|
|
27
34
|
DEFAULT_API_BASE,
|
|
28
35
|
DEFAULT_API_KEY,
|
|
36
|
+
DEFAULT_CHALLENGER_MODEL,
|
|
37
|
+
DEFAULT_FINDER_MODEL,
|
|
38
|
+
DEFAULT_JUDGE_MODEL,
|
|
29
39
|
DEFAULT_MODEL,
|
|
30
40
|
PROVIDERS,
|
|
31
41
|
make_provider,
|
|
@@ -36,6 +46,31 @@ _FORMATS = ("text", "markdown", "json", "sarif")
|
|
|
36
46
|
_FAIL_ON = ("critical", "high", "medium", "low")
|
|
37
47
|
|
|
38
48
|
|
|
49
|
+
def _split_diff_by_file(diff: str) -> list[str]:
|
|
50
|
+
"""Split a unified diff into one diff per file (`diff --git ...` boundaries)."""
|
|
51
|
+
chunks: list[str] = []
|
|
52
|
+
cur: list[str] = []
|
|
53
|
+
for line in diff.splitlines(keepends=True):
|
|
54
|
+
if line.startswith("diff --git ") and cur:
|
|
55
|
+
chunks.append("".join(cur))
|
|
56
|
+
cur = []
|
|
57
|
+
cur.append(line)
|
|
58
|
+
if cur:
|
|
59
|
+
chunks.append("".join(cur))
|
|
60
|
+
return chunks or ([diff] if diff.strip() else [])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _dedup_findings(findings: list[Finding]) -> list[Finding]:
|
|
64
|
+
seen: set = set()
|
|
65
|
+
out: list[Finding] = []
|
|
66
|
+
for f in findings:
|
|
67
|
+
k = (f.file, f.line, f.category, f.description)
|
|
68
|
+
if k not in seen:
|
|
69
|
+
seen.add(k)
|
|
70
|
+
out.append(f)
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
|
|
39
74
|
def audit_diff(
|
|
40
75
|
diff: str,
|
|
41
76
|
*,
|
|
@@ -44,12 +79,30 @@ def audit_diff(
|
|
|
44
79
|
mode: str = "standard",
|
|
45
80
|
max_rounds: int = 3,
|
|
46
81
|
filter_findings: bool = True,
|
|
82
|
+
finder_model: str | None = None,
|
|
83
|
+
challenger_model: str | None = None,
|
|
84
|
+
judge_model: str | None = None,
|
|
47
85
|
) -> tuple[list[Finding], list[tuple[Finding, str]]]:
|
|
48
|
-
"""Audit a diff and return (kept findings, dropped (finding, reason)).
|
|
49
|
-
|
|
50
|
-
|
|
86
|
+
"""Audit a diff and return (kept findings, dropped (finding, reason)).
|
|
87
|
+
|
|
88
|
+
A diff over the size budget is audited one file at a time so it does not
|
|
89
|
+
overflow the context. Finding categories are normalized to the rule-id set."""
|
|
90
|
+
def _run_one(d: str) -> list[Finding]:
|
|
91
|
+
if mode == "adversarial":
|
|
92
|
+
return AdversarialAuditRunner(
|
|
93
|
+
provider=provider, model=model,
|
|
94
|
+
finder_model=finder_model, challenger_model=challenger_model, judge_model=judge_model,
|
|
95
|
+
).run(d, max_rounds=max_rounds).findings
|
|
96
|
+
return AuditRunner(provider=provider, model=model).run(d)
|
|
97
|
+
|
|
98
|
+
if len(diff) > _MAX_DIFF_CHARS:
|
|
99
|
+
chunks = _split_diff_by_file(diff)
|
|
100
|
+
findings = _dedup_findings([f for c in chunks for f in _run_one(c)])
|
|
51
101
|
else:
|
|
52
|
-
findings =
|
|
102
|
+
findings = _run_one(diff)
|
|
103
|
+
|
|
104
|
+
allowed = set(allowed_categories())
|
|
105
|
+
findings = [dataclasses.replace(f, category=normalize_category(f.category, allowed)) for f in findings]
|
|
53
106
|
if filter_findings:
|
|
54
107
|
return FindingsFilter().filter(findings)
|
|
55
108
|
return findings, []
|
|
@@ -85,9 +138,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
85
138
|
a.add_argument("--rounds", type=int, default=3, help="adversarial debate rounds")
|
|
86
139
|
a.add_argument("--provider", choices=PROVIDERS, default="anthropic")
|
|
87
140
|
a.add_argument("--model", default=DEFAULT_MODEL)
|
|
141
|
+
a.add_argument("--finder-model", default=DEFAULT_FINDER_MODEL, help="adversarial: finder role model (default: --model)")
|
|
142
|
+
a.add_argument("--challenger-model", default=DEFAULT_CHALLENGER_MODEL, help="adversarial: challenger role model")
|
|
143
|
+
a.add_argument("--judge-model", default=DEFAULT_JUDGE_MODEL, help="adversarial: judge role model")
|
|
88
144
|
a.add_argument("--api-base", default=DEFAULT_API_BASE)
|
|
89
145
|
a.add_argument("--api-key", default=DEFAULT_API_KEY)
|
|
90
|
-
a.add_argument("--retries", type=int, default=
|
|
146
|
+
a.add_argument("--retries", type=int, default=2, help="provider retry attempts on transient failure")
|
|
91
147
|
a.add_argument("--format", choices=_FORMATS, default="text", dest="fmt")
|
|
92
148
|
a.add_argument("--no-filter", action="store_true", help="skip the false-positive filter")
|
|
93
149
|
a.add_argument("--fail-on", choices=_FAIL_ON, default=None, dest="fail_on")
|
|
@@ -110,6 +166,7 @@ def _dispatch(args, parser) -> int:
|
|
|
110
166
|
kept, _ = audit_diff(
|
|
111
167
|
_read_diff(args), provider=provider, model=args.model,
|
|
112
168
|
mode=args.mode, max_rounds=args.rounds, filter_findings=not args.no_filter,
|
|
169
|
+
finder_model=args.finder_model, challenger_model=args.challenger_model, judge_model=args.judge_model,
|
|
113
170
|
)
|
|
114
171
|
print(render(args.fmt, kept))
|
|
115
172
|
return 1 if gate(kept, args.fail_on) else 0
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-security-review
|
|
3
|
+
description: "Application security rules for reviewing code for exploitable vulnerabilities. Use when reviewing a diff or a codebase for security issues, or when writing code that handles untrusted input, authentication, authorization, file paths, database queries, network requests, deserialization, or cryptography. Read the matching rule before judging whether code is vulnerable."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Code Security Review Rules
|
|
7
|
+
|
|
8
|
+
Application-security rules, one file per weakness class under `rules/`, named by
|
|
9
|
+
the specific weakness (CWE-style). Each rule states impact, the markers to hunt
|
|
10
|
+
(`triggers`), and vulnerable-vs-secure examples. The diff-audit engine injects the
|
|
11
|
+
rules relevant to a change into the prompt; the full-review agent reads them for
|
|
12
|
+
the target's stack. A finding's `category` is one of these ids.
|
|
13
|
+
|
|
14
|
+
## Rules by OWASP category
|
|
15
|
+
|
|
16
|
+
### A01 Broken Access Control
|
|
17
|
+
- `missing-authorization` (CWE-862)
|
|
18
|
+
- `insecure-direct-object-reference` (CWE-639)
|
|
19
|
+
- `cross-site-request-forgery` (CWE-352)
|
|
20
|
+
- `path-traversal` (CWE-22)
|
|
21
|
+
- `open-redirect` (CWE-601)
|
|
22
|
+
|
|
23
|
+
### A02 Cryptographic Failures
|
|
24
|
+
- `insecure-cryptography` (CWE-327)
|
|
25
|
+
- `insecure-transport` (CWE-319)
|
|
26
|
+
- `hardcoded-secrets` (CWE-798)
|
|
27
|
+
- `information-exposure` (CWE-200/532)
|
|
28
|
+
|
|
29
|
+
### A03 Injection
|
|
30
|
+
- `sql-injection` (CWE-89)
|
|
31
|
+
- `command-injection` (CWE-78)
|
|
32
|
+
- `code-injection` (CWE-94)
|
|
33
|
+
- `cross-site-scripting` (CWE-79)
|
|
34
|
+
- `xml-external-entity` (CWE-611)
|
|
35
|
+
- `server-side-template-injection` (CWE-1336)
|
|
36
|
+
- `http-response-splitting` (CWE-113)
|
|
37
|
+
|
|
38
|
+
### A04 Insecure Design / Business Logic
|
|
39
|
+
- `business-logic` (CWE-840)
|
|
40
|
+
- `replay-attack` (CWE-294)
|
|
41
|
+
- `race-condition` (CWE-362)
|
|
42
|
+
- `mass-assignment` (CWE-915)
|
|
43
|
+
|
|
44
|
+
### A07 Identification and Authentication
|
|
45
|
+
- `improper-authentication` (CWE-287)
|
|
46
|
+
- `jwt-validation` (CWE-347)
|
|
47
|
+
- `session-fixation` (CWE-384)
|
|
48
|
+
|
|
49
|
+
### A08 Software and Data Integrity
|
|
50
|
+
- `insecure-deserialization` (CWE-502)
|
|
51
|
+
|
|
52
|
+
### A10 Server-Side Request Forgery
|
|
53
|
+
- `server-side-request-forgery` (CWE-918)
|
|
54
|
+
|
|
55
|
+
Report only real, exploitable, high-confidence issues with a concrete exploit
|
|
56
|
+
path. Do not report dependency CVEs, style, speculation, or config-leak-only
|
|
57
|
+
risks. The set is data: add a class by dropping a new `rules/<id>.md` with the
|
|
58
|
+
same frontmatter (id, title, impact, tags, triggers) and examples.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: business-logic
|
|
3
|
+
title: Business Logic Flaw
|
|
4
|
+
impact: HIGH
|
|
5
|
+
tags: [cwe-840, cwe-841, owasp-a04]
|
|
6
|
+
triggers: ["price", "amount", "quantity", "balance", "status", "approve", "discount", "total =", "request.json[\"price\"", "if status =="]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Business Logic Flaw
|
|
10
|
+
|
|
11
|
+
A stateful workflow trusts client-supplied values or does not enforce its own rules: a client-set price/amount/quantity, a state-machine step reached without its prerequisites, an approval or limit bypassed. Validate amounts and state transitions server-side from trusted data, and enforce the workflow's invariants.
|
|
12
|
+
|
|
13
|
+
### Python
|
|
14
|
+
Vulnerable:
|
|
15
|
+
```python
|
|
16
|
+
total = request.json["price"] * request.json["qty"] # client sets the price
|
|
17
|
+
order.charge(total)
|
|
18
|
+
```
|
|
19
|
+
Secure:
|
|
20
|
+
```python
|
|
21
|
+
price = Product.objects.get(id=pid).price # price from the server
|
|
22
|
+
total = price * validate_qty(request.json["qty"])
|
|
23
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: code-injection
|
|
3
|
+
title: Code Injection
|
|
4
|
+
impact: CRITICAL
|
|
5
|
+
tags: [cwe-94, owasp-a03, injection, rce]
|
|
6
|
+
triggers: ["eval(", "exec(", "compile(", "pickle.loads", "new Function", "setTimeout(\"", "vm.runInContext"]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Code Injection
|
|
10
|
+
|
|
11
|
+
Passing untrusted input to a language evaluation primitive (eval, exec, compile, dynamic import, JS Function) lets an attacker execute arbitrary code. Never evaluate untrusted input; parse it with a data-only parser or dispatch through an allowlist.
|
|
12
|
+
|
|
13
|
+
### Python
|
|
14
|
+
Vulnerable:
|
|
15
|
+
```python
|
|
16
|
+
result = eval(request.args["expr"])
|
|
17
|
+
exec(user_supplied_code)
|
|
18
|
+
```
|
|
19
|
+
Secure:
|
|
20
|
+
```python
|
|
21
|
+
import ast
|
|
22
|
+
result = ast.literal_eval(request.args["expr"]) # data only, no code
|
|
23
|
+
```
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
+
id: command-injection
|
|
2
3
|
title: Command Injection
|
|
3
4
|
impact: CRITICAL
|
|
4
|
-
tags: [
|
|
5
|
-
triggers: ["os.system", "os.popen", "subprocess", "shell=True", "
|
|
5
|
+
tags: [cwe-78, owasp-a03, injection]
|
|
6
|
+
triggers: ["os.system", "os.popen", "subprocess", "shell=True", "Runtime.getRuntime", "child_process", "popen", "exec("]
|
|
6
7
|
---
|
|
7
8
|
|
|
8
9
|
## Command Injection
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: cross-site-request-forgery
|
|
3
|
+
title: Cross-Site Request Forgery
|
|
4
|
+
impact: HIGH
|
|
5
|
+
tags: [cwe-352, owasp-a01]
|
|
6
|
+
triggers: ["@app.route", "methods=[\"POST\"", "csrf", "SameSite", "csrf_exempt", "@csrf", "form"]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Cross-Site Request Forgery
|
|
10
|
+
|
|
11
|
+
A state-changing request is accepted using only ambient credentials (a session cookie) with no anti-CSRF token or SameSite protection, so a malicious site can make the victim's browser perform the action. Require a CSRF token (or SameSite=strict/lax cookies + origin check) on every state-changing endpoint.
|
|
12
|
+
|
|
13
|
+
### Python
|
|
14
|
+
Vulnerable:
|
|
15
|
+
```python
|
|
16
|
+
@app.route("/account/email", methods=["POST"])
|
|
17
|
+
@csrf.exempt # disables CSRF protection on a state-changing route
|
|
18
|
+
def change_email():
|
|
19
|
+
current_user.email = request.form["email"]; db.commit()
|
|
20
|
+
```
|
|
21
|
+
Secure: keep CSRF protection on; validate the token, or set the session cookie `SameSite="Lax"` and check the Origin header.
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
2
|
+
id: cross-site-scripting
|
|
3
|
+
title: Cross-Site Scripting
|
|
3
4
|
impact: HIGH
|
|
4
|
-
tags: [
|
|
5
|
+
tags: [cwe-79, owasp-a03, injection]
|
|
5
6
|
triggers: ["innerHTML", "dangerouslySetInnerHTML", "|safe", "mark_safe", "render_template_string", "v-html", "document.write", "Markup("]
|
|
6
7
|
---
|
|
7
8
|
|
|
8
|
-
## Cross-Site Scripting
|
|
9
|
+
## Cross-Site Scripting
|
|
9
10
|
|
|
10
|
-
Untrusted data rendered into HTML without encoding executes as script in the victim's browser. Render data as text
|
|
11
|
+
Untrusted data rendered into HTML without context-aware encoding executes as script in the victim's browser. Render data as text, rely on framework auto-escaping, and never disable it for user data.
|
|
11
12
|
|
|
12
13
|
### JavaScript
|
|
13
14
|
Vulnerable:
|
|
@@ -21,4 +22,4 @@ el.textContent = "Hello " + username;
|
|
|
21
22
|
|
|
22
23
|
### Python (templates)
|
|
23
24
|
Vulnerable: `return render_template_string("<div>" + user_input + "</div>")`
|
|
24
|
-
Secure: rely on Jinja auto-escaping; never
|
|
25
|
+
Secure: rely on Jinja auto-escaping; never apply `| safe` to user data.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: hardcoded-secrets
|
|
3
|
+
title: Hardcoded Secrets
|
|
4
|
+
impact: HIGH
|
|
5
|
+
tags: [cwe-798, cwe-259, owasp-a02]
|
|
6
|
+
triggers: ["api_key =", "API_KEY =", "password =", "secret =", "token =", "sk_live", "ghp_", "aws_secret"]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Hardcoded Secrets
|
|
10
|
+
|
|
11
|
+
A literal credential, key, or token in source leaks with the code and cannot be rotated easily. Load secrets from environment variables or a secret manager. A variable that reads from the environment or is passed in as a parameter is fine; only an actual literal value is a finding.
|
|
12
|
+
|
|
13
|
+
### Python
|
|
14
|
+
Vulnerable:
|
|
15
|
+
```python
|
|
16
|
+
API_KEY = "sk_live_51HxQ...actual-secret"
|
|
17
|
+
```
|
|
18
|
+
Secure:
|
|
19
|
+
```python
|
|
20
|
+
api_key = os.environ["API_KEY"]
|
|
21
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: http-response-splitting
|
|
3
|
+
title: HTTP Response Splitting
|
|
4
|
+
impact: MEDIUM
|
|
5
|
+
tags: [cwe-113, cwe-93, owasp-a03]
|
|
6
|
+
triggers: ["set_header", "add_header", "Location", "Set-Cookie", "response.headers", "resp.headers", "make_response", "setHeader"]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## HTTP Response Splitting / Header Injection
|
|
10
|
+
|
|
11
|
+
Putting untrusted input into a response header (or redirect Location, Set-Cookie) without stripping CR/LF lets an attacker inject headers or split the response. Strip or reject newline characters in any header value built from input; frameworks often do this, but manual header construction may not.
|
|
12
|
+
|
|
13
|
+
### Python
|
|
14
|
+
Vulnerable:
|
|
15
|
+
```python
|
|
16
|
+
resp.headers["X-Echo"] = request.args["v"] # v may contain \r\n
|
|
17
|
+
```
|
|
18
|
+
Secure:
|
|
19
|
+
```python
|
|
20
|
+
v = request.args["v"]
|
|
21
|
+
if "\n" in v or "\r" in v:
|
|
22
|
+
abort(400)
|
|
23
|
+
resp.headers["X-Echo"] = v
|
|
24
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: improper-authentication
|
|
3
|
+
title: Improper Authentication
|
|
4
|
+
impact: HIGH
|
|
5
|
+
tags: [cwe-287, cwe-306, owasp-a07]
|
|
6
|
+
triggers: ["login", "authenticate", "password ==", "== token", "auth", "bypass", "if not user", "verify_password"]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Improper Authentication
|
|
10
|
+
|
|
11
|
+
Authentication is missing on a protected path, can be bypassed by a logic flaw, or compares credentials unsafely (a hardcoded bypass token, a non-constant-time compare, trusting a client-asserted identity). Verify identity server-side against a trusted store before granting access; compare secrets in constant time.
|
|
12
|
+
|
|
13
|
+
### Python
|
|
14
|
+
Vulnerable:
|
|
15
|
+
```python
|
|
16
|
+
if request.headers.get("X-Auth") == "debug-bypass": # hardcoded bypass
|
|
17
|
+
return admin_dashboard()
|
|
18
|
+
if user.token == request.args["token"]: # non-constant-time, and client-asserted
|
|
19
|
+
login(user)
|
|
20
|
+
```
|
|
21
|
+
Secure:
|
|
22
|
+
```python
|
|
23
|
+
import hmac
|
|
24
|
+
if user and hmac.compare_digest(user.token, request.args["token"]):
|
|
25
|
+
login(user)
|
|
26
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: information-exposure
|
|
3
|
+
title: Information Exposure
|
|
4
|
+
impact: MEDIUM
|
|
5
|
+
tags: [cwe-200, cwe-532, cwe-209, owasp-a02]
|
|
6
|
+
triggers: ["traceback.format_exc", "str(e)", "log.info(token", "logger.debug(secret", "print(password", "DEBUG = True", "jsonify(error="]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Information Exposure
|
|
10
|
+
|
|
11
|
+
Sensitive data (secrets, tokens, PII) written to logs, or internal detail (stack traces, exception messages, debug output) returned to the client, helps an attacker and widens breach impact. Log only non-secret data, return a generic error to the caller, and keep detail server-side.
|
|
12
|
+
|
|
13
|
+
### Python
|
|
14
|
+
Vulnerable:
|
|
15
|
+
```python
|
|
16
|
+
logger.info("auth token: %s", token)
|
|
17
|
+
return jsonify(error=traceback.format_exc()), 500
|
|
18
|
+
```
|
|
19
|
+
Secure:
|
|
20
|
+
```python
|
|
21
|
+
logger.info("auth attempt for user %s", user_id)
|
|
22
|
+
app.logger.exception("auth failed"); return jsonify(error="internal error"), 500
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Not a finding (do not flag)
|
|
26
|
+
|
|
27
|
+
This is about leaking secrets/PII or internal detail. It is not a finding to:
|
|
28
|
+
- read or serve a file, make a request, or return ordinary application data,
|
|
29
|
+
- log non-sensitive identifiers (a user id, a request path, a status).
|
|
30
|
+
|
|
31
|
+
Reaching a file or returning a record is only information exposure when the data
|
|
32
|
+
returned is itself sensitive (a secret, another user's PII) or internal (a stack
|
|
33
|
+
trace, a query). A plain `open(...)` or response is not this weakness.
|
|
34
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: insecure-cryptography
|
|
3
|
+
title: Insecure Cryptography
|
|
4
|
+
impact: HIGH
|
|
5
|
+
tags: [cwe-327, cwe-916, owasp-a02]
|
|
6
|
+
triggers: ["hashlib.md5", "hashlib.sha1", "DES", "ECB", "MODE_ECB", "random.random", "random.randint", "md5(", "sha1("]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Insecure Cryptography
|
|
10
|
+
|
|
11
|
+
Weak algorithms (MD5, SHA-1, DES, RC4), ECB mode, fast hashes for passwords, static or reused IVs, and non-cryptographic randomness for security values are exploitable. Use AES-GCM or ChaCha20-Poly1305, bcrypt/scrypt/argon2 for passwords, a fresh random nonce per message, and a CSPRNG (secrets / os.urandom).
|
|
12
|
+
|
|
13
|
+
### Python
|
|
14
|
+
Vulnerable:
|
|
15
|
+
```python
|
|
16
|
+
hashlib.md5(password.encode()).hexdigest()
|
|
17
|
+
token = str(random.randint(0, 999999))
|
|
18
|
+
```
|
|
19
|
+
Secure:
|
|
20
|
+
```python
|
|
21
|
+
bcrypt.hashpw(password.encode(), bcrypt.gensalt())
|
|
22
|
+
token = secrets.token_urlsafe(32)
|
|
23
|
+
```
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
+
id: insecure-deserialization
|
|
2
3
|
title: Insecure Deserialization
|
|
3
4
|
impact: CRITICAL
|
|
4
|
-
tags: [
|
|
5
|
+
tags: [cwe-502, owasp-a08, rce]
|
|
5
6
|
triggers: ["pickle.loads", "pickle.load", "yaml.load", "marshal.loads", "jsonpickle", "ObjectInputStream", "torch.load"]
|
|
6
7
|
---
|
|
7
8
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: insecure-direct-object-reference
|
|
3
|
+
title: Insecure Direct Object Reference
|
|
4
|
+
impact: HIGH
|
|
5
|
+
tags: [cwe-639, owasp-a01, access-control]
|
|
6
|
+
triggers: ["objects.get(", "findById", "get_object_or_404", "/<id>", "/:id", "request.args", "params[", "pk=", "where id ="]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Insecure Direct Object Reference
|
|
10
|
+
|
|
11
|
+
A record is fetched or mutated by a user-supplied id without checking the caller owns or may access it, so an authenticated user reaches another user's, tenant's, or service's data by changing the id. Scope every object lookup to the caller's identity or tenant.
|
|
12
|
+
|
|
13
|
+
### Python (Django)
|
|
14
|
+
Vulnerable:
|
|
15
|
+
```python
|
|
16
|
+
account = Account.objects.get(id=request.GET["account_id"])
|
|
17
|
+
```
|
|
18
|
+
Secure:
|
|
19
|
+
```python
|
|
20
|
+
account = get_object_or_404(Account, id=request.GET["account_id"], owner=request.user)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Node.js (Express)
|
|
24
|
+
Vulnerable: `const doc = await Document.findById(req.params.id)`
|
|
25
|
+
Secure: `const doc = await Document.findOne({ _id: req.params.id, userId: req.user.id })`
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: insecure-transport
|
|
3
|
+
title: Insecure Transport
|
|
4
|
+
impact: HIGH
|
|
5
|
+
tags: [cwe-319, cwe-295, owasp-a02]
|
|
6
|
+
triggers: ["http://", "verify=False", "CERT_NONE", "check_hostname", "_create_unverified", "rejectUnauthorized", "InsecureSkipVerify"]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Insecure Transport
|
|
10
|
+
|
|
11
|
+
Sending sensitive data over cleartext HTTP, or disabling TLS certificate/hostname verification, exposes it to interception and man-in-the-middle. Use HTTPS and leave certificate verification on (the secure default).
|
|
12
|
+
|
|
13
|
+
### Python
|
|
14
|
+
Vulnerable:
|
|
15
|
+
```python
|
|
16
|
+
requests.get("https://api.example.com/data", verify=False)
|
|
17
|
+
requests.post("http://api.example.com/login", data=creds)
|
|
18
|
+
```
|
|
19
|
+
Secure:
|
|
20
|
+
```python
|
|
21
|
+
requests.get("https://api.example.com/data") # verify defaults to True
|
|
22
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: jwt-validation
|
|
3
|
+
title: JWT Validation Flaw
|
|
4
|
+
impact: HIGH
|
|
5
|
+
tags: [cwe-347, cwe-345, owasp-a07]
|
|
6
|
+
triggers: ["jwt.decode", "verify=False", "verify_signature", "algorithms", "none", "decode("]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## JWT Validation Flaw
|
|
10
|
+
|
|
11
|
+
Accepting a JWT without verifying its signature, allowing the "none" algorithm, or reading claims before verification lets an attacker forge identity. Verify the signature with a fixed algorithm and validate iss/aud/exp before using any claim.
|
|
12
|
+
|
|
13
|
+
### Python
|
|
14
|
+
Vulnerable:
|
|
15
|
+
```python
|
|
16
|
+
claims = jwt.decode(token, options={"verify_signature": False})
|
|
17
|
+
jwt.decode(token, key, algorithms=["none"])
|
|
18
|
+
```
|
|
19
|
+
Secure:
|
|
20
|
+
```python
|
|
21
|
+
claims = jwt.decode(token, key, algorithms=["RS256"], audience=AUD, issuer=ISS)
|
|
22
|
+
```
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
+
id: mass-assignment
|
|
2
3
|
title: Mass Assignment
|
|
3
4
|
impact: HIGH
|
|
4
|
-
tags: [
|
|
5
|
-
triggers: ["(**request", "update(**", "setattr(", "Object.assign", "
|
|
5
|
+
tags: [cwe-915, owasp-a04, owasp-a08]
|
|
6
|
+
triggers: ["(**request", "update(**", "setattr(", "Object.assign", "create(**", "request.get_json", ".save()"]
|
|
6
7
|
---
|
|
7
8
|
|
|
8
9
|
## Mass Assignment
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: missing-authorization
|
|
3
|
+
title: Missing Authorization
|
|
4
|
+
impact: HIGH
|
|
5
|
+
tags: [cwe-862, owasp-a01, access-control]
|
|
6
|
+
triggers: ["@app.route", "@router", "@login_required", "requires_", "permission", "is_admin", "role", "def delete", "def admin"]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Missing Authorization
|
|
10
|
+
|
|
11
|
+
A privileged or state-changing endpoint performs its action without verifying the caller is allowed to, or derives the role/permission from a client-controlled value. Enforce authorization server-side, per request, from a trusted store; a route that mutates or exposes privileged data must check the caller's rights the same way its peers do.
|
|
12
|
+
|
|
13
|
+
### Python
|
|
14
|
+
Vulnerable:
|
|
15
|
+
```python
|
|
16
|
+
@app.route("/admin/users/<uid>", methods=["DELETE"])
|
|
17
|
+
def delete_user(uid): # no authorization check
|
|
18
|
+
User.objects.get(id=uid).delete()
|
|
19
|
+
|
|
20
|
+
is_admin = request.json["is_admin"] # privilege from the client
|
|
21
|
+
```
|
|
22
|
+
Secure:
|
|
23
|
+
```python
|
|
24
|
+
@app.route("/admin/users/<uid>", methods=["DELETE"])
|
|
25
|
+
@requires_admin
|
|
26
|
+
def delete_user(uid):
|
|
27
|
+
User.objects.get(id=uid).delete()
|
|
28
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: open-redirect
|
|
3
|
+
title: Open Redirect
|
|
4
|
+
impact: MEDIUM
|
|
5
|
+
tags: [cwe-601, owasp-a01]
|
|
6
|
+
triggers: ["redirect(", "Location", "next=", "return_url", "redirect_uri", "sendRedirect", "res.redirect"]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Open Redirect
|
|
10
|
+
|
|
11
|
+
A redirect target taken from untrusted input without validation lets an attacker send victims to an attacker-controlled site (phishing, OAuth token theft). Validate the target against an allowlist of paths/hosts, or only allow relative paths.
|
|
12
|
+
|
|
13
|
+
### Python
|
|
14
|
+
Vulnerable:
|
|
15
|
+
```python
|
|
16
|
+
return redirect(request.args["next"])
|
|
17
|
+
```
|
|
18
|
+
Secure:
|
|
19
|
+
```python
|
|
20
|
+
target = request.args["next"]
|
|
21
|
+
if not target.startswith("/") or target.startswith("//"):
|
|
22
|
+
target = "/"
|
|
23
|
+
return redirect(target)
|
|
24
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: path-traversal
|
|
3
|
+
title: Path Traversal
|
|
4
|
+
impact: HIGH
|
|
5
|
+
tags: [cwe-22, owasp-a01]
|
|
6
|
+
triggers: ["open(", "os.path.join", "send_file", "sendfile", "readFile", "filename", "../", "upload"]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Path Traversal
|
|
10
|
+
|
|
11
|
+
A filesystem path built from untrusted input without containment lets `../` escape the intended directory. Resolve the path and confirm it stays within an allowed base, or use only the basename.
|
|
12
|
+
|
|
13
|
+
### Python
|
|
14
|
+
Vulnerable:
|
|
15
|
+
```python
|
|
16
|
+
open(os.path.join(UPLOAD_DIR, request.args["filename"]))
|
|
17
|
+
```
|
|
18
|
+
Secure:
|
|
19
|
+
```python
|
|
20
|
+
target = (UPLOAD_DIR / filename).resolve()
|
|
21
|
+
if not target.is_relative_to(UPLOAD_DIR):
|
|
22
|
+
raise ValueError("path escapes base dir")
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Not a finding (do not flag)
|
|
26
|
+
|
|
27
|
+
If the input is neutralized before the file operation there is no traversal:
|
|
28
|
+
- `os.path.basename(name)` is applied (strips `../` and any directory parts),
|
|
29
|
+
- the resolved path is confirmed within a base (`is_relative_to`, `realpath` under base),
|
|
30
|
+
- the value is from an allowlist, or is a constant / trusted-config path.
|
|
31
|
+
|
|
32
|
+
`open(os.path.join(BASE, os.path.basename(name)))` is safe: basename removes the
|
|
33
|
+
traversal. Do not report it.
|
|
34
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: race-condition
|
|
3
|
+
title: Race Condition (TOCTOU)
|
|
4
|
+
impact: HIGH
|
|
5
|
+
tags: [cwe-362, cwe-367, owasp-a04]
|
|
6
|
+
triggers: ["if balance", "balance -=", "select_for_update", "get(...).save", "check", "transaction", "lock", "atomic"]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Race Condition / TOCTOU
|
|
10
|
+
|
|
11
|
+
A check and the action it guards run on shared state without a lock or atomic update, so two concurrent requests both pass the check (double-spend, double-redeem, limit bypass). Use a row lock, an atomic conditional update, or a transaction.
|
|
12
|
+
|
|
13
|
+
### Python (Django)
|
|
14
|
+
Vulnerable:
|
|
15
|
+
```python
|
|
16
|
+
acct = Account.objects.get(pk=pk)
|
|
17
|
+
if acct.balance >= amount:
|
|
18
|
+
acct.balance -= amount; acct.save() # concurrent requests both pass
|
|
19
|
+
```
|
|
20
|
+
Secure:
|
|
21
|
+
```python
|
|
22
|
+
with transaction.atomic():
|
|
23
|
+
acct = Account.objects.select_for_update().get(pk=pk)
|
|
24
|
+
if acct.balance >= amount:
|
|
25
|
+
acct.balance -= amount; acct.save()
|
|
26
|
+
```
|