File size: 12,298 Bytes
9b30c36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
EASY_SAMPLE_1 = {
    "filename": "user_service.py",
    "language": "python",
    "content": """\
import hashlib
from typing import List


class UserService:
    def __init__(self, users: dict):
        self.users = users

    def get_user_name(self, user_id: int) -> str:
        # BUG-1 (line 10): KeyError not handled
        return self.users[user_id]["name"]

    def get_active_users(self, users: List[dict]) -> List[dict]:
        result = []
        # BUG-2 (line 15): off-by-one, skips last user
        for i in range(len(users) - 1):
            if users[i].get("active"):
                result.append(users[i])
        return result

    def compute_checksum(self, data: str) -> str:
        # BUG-3 (line 22): MD5 is cryptographically broken
        return hashlib.md5(data.encode()).hexdigest()

    def update_email(self, user_id: int, new_email: str) -> bool:
        user = self.users.get(user_id)
        if user:
            user["email"] == new_email  # BUG-4 (line 27): == instead of =
            return True
        return False

    def calculate_average_age(self, user_ids: List[int]) -> float:
        total = 0
        for uid in user_ids:
            total += self.users[uid]["age"]
        # BUG-5 (line 34): ZeroDivisionError if user_ids is empty
        return total / len(user_ids)
""",
    "ground_truth_bugs": [
        {"line": 10, "type": "missing_error_handling", "severity": "high",
         "description": "KeyError not caught; user_id may not exist"},
        {"line": 15, "type": "off_by_one", "severity": "medium",
         "description": "range(len(users)-1) skips the last element"},
        {"line": 22, "type": "weak_cryptography", "severity": "high",
         "description": "MD5 is broken; use hashlib.sha256"},
        {"line": 27, "type": "assignment_operator", "severity": "high",
         "description": "== used instead of =; update silently ignored"},
        {"line": 34, "type": "zero_division", "severity": "medium",
         "description": "ZeroDivisionError when user_ids is empty"},
    ],
}

EASY_SAMPLE_2 = {
    "filename": "order_processor.py",
    "language": "python",
    "content": """\
from datetime import datetime
from typing import List, Dict, Any

DISCOUNT_RATES = {"gold": 0.20, "silver": 0.10, "bronze": 0.05}


def apply_discount(price: float, tier: str) -> float:
    # BUG-1 (line 8): KeyError if tier not in DISCOUNT_RATES
    discount = DISCOUNT_RATES[tier]
    return price * (1 - discount)


def process_orders(orders: List[Dict[str, Any]]) -> List[Dict]:
    results = []
    for order in orders:
        # BUG-2 (line 15): mutates original dict in-place
        order["processed"] = True
        order["timestamp"] = datetime.now().isoformat()
        results.append(order)
    return results


def find_duplicate_orders(orders: List[Dict]) -> List[str]:
    seen = []
    duplicates = []
    for order in orders:
        oid = order["order_id"]
        # BUG-3 (line 25): O(n^2) list scan; should be a set
        if oid in seen:
            duplicates.append(oid)
        seen.append(oid)
    return duplicates


def calculate_total(items: List[Dict]) -> float:
    # BUG-4 (line 32): no guard for empty list
    return sum(i["price"] * i["quantity"] for i in items) / len(items)


def format_currency(amount: float) -> str:
    # BUG-5 (line 36): float imprecision for financial data; use Decimal
    return f"${amount:.2f}"
""",
    "ground_truth_bugs": [
        {"line": 8,  "type": "missing_error_handling", "severity": "high",
         "description": "KeyError if tier not in DISCOUNT_RATES"},
        {"line": 15, "type": "mutation_side_effect", "severity": "medium",
         "description": "Mutates original dict instead of copying"},
        {"line": 25, "type": "performance", "severity": "low",
         "description": "O(n^2) list scan; use a set for O(1) lookup"},
        {"line": 32, "type": "zero_division", "severity": "medium",
         "description": "ZeroDivisionError when items is empty"},
        {"line": 36, "type": "financial_precision", "severity": "medium",
         "description": "Float imprecision for financial data; use Decimal"},
    ],
}

MEDIUM_SAMPLE_1 = {
    "filename": "auth_api.py",
    "language": "python",
    "content": """\
import sqlite3
from flask import Flask, request, jsonify, session

app = Flask(__name__)
# VULN-1 (line 5): hardcoded secret key
app.secret_key = "super_secret_key_123"

# VULN-2 (line 8): hardcoded DB password
DB_PASSWORD = "admin123"


def get_db():
    return sqlite3.connect("users.db")


@app.route("/login", methods=["POST"])
def login():
    username = request.form.get("username")
    password = request.form.get("password")
    db = get_db()
    # VULN-3 (line 20): SQL Injection via f-string
    query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
    user = db.execute(query).fetchone()
    if user:
        session["user_id"] = user[0]
        return jsonify({"status": "ok"})
    return jsonify({"status": "invalid"}), 401


@app.route("/search", methods=["GET"])
def search():
    term = request.args.get("q", "")
    # VULN-4 (line 30): Reflected XSS
    return f"<html><body><h1>Results for: {term}</h1></body></html>"


@app.route("/profile", methods=["GET"])
def profile():
    user_id = request.args.get("user_id")
    # VULN-5 (line 36): IDOR - no authorization check
    db = get_db()
    user = db.execute(f"SELECT * FROM users WHERE id={user_id}").fetchone()
    return jsonify({"user": user})


@app.route("/upload", methods=["POST"])
def upload():
    file = request.files["file"]
    # VULN-6 (line 43): Unrestricted file upload
    file.save(f"/uploads/{file.filename}")
    return jsonify({"status": "uploaded"})


if __name__ == "__main__":
    # VULN-7 (line 48): Debug mode in production
    app.run(debug=True, host="0.0.0.0")
""",
    "ground_truth_vulnerabilities": [
        {"line": 5,  "category": "A07_Identification_Failures",
         "owasp": "A07:2021", "severity": "critical",
         "description": "Hardcoded secret key in source"},
        {"line": 8,  "category": "A02_Cryptographic_Failures",
         "owasp": "A02:2021", "severity": "critical",
         "description": "Hardcoded DB password"},
        {"line": 20, "category": "A03_Injection",
         "owasp": "A03:2021", "severity": "critical",
         "description": "SQL Injection via f-string interpolation"},
        {"line": 30, "category": "A03_Injection",
         "owasp": "A03:2021", "severity": "high",
         "description": "Reflected XSS β€” unescaped user input in HTML"},
        {"line": 36, "category": "A01_Broken_Access_Control",
         "owasp": "A01:2021", "severity": "high",
         "description": "IDOR β€” no authorization before profile access"},
        {"line": 43, "category": "A04_Insecure_Design",
         "owasp": "A04:2021", "severity": "high",
         "description": "Unrestricted file upload, no type check"},
        {"line": 48, "category": "A05_Security_Misconfiguration",
         "owasp": "A05:2021", "severity": "medium",
         "description": "Flask debug mode exposes Werkzeug debugger"},
    ],
}

HARD_SAMPLE_1 = {
    "files": [
        {
            "filename": "services/payment_service.py",
            "language": "python",
            "content": """\
import requests

PAYMENT_API_URL = "https://api.payments.io/v1"
API_KEY = "pk_live_abc123xyz"   # hardcoded production key


def charge_card(card_token: str, amount: int, currency: str = "USD"):
    # No retry, no timeout
    response = requests.post(
        f"{PAYMENT_API_URL}/charges",
        json={"token": card_token, "amount": amount, "currency": currency},
        headers={"Authorization": f"Bearer {API_KEY}"},
    )
    if response.status_code == 200:
        return response.json()
    raise Exception(f"Payment failed: {response.text}")


def refund_charge(charge_id: str, amount: int):
    # No idempotency key
    response = requests.post(
        f"{PAYMENT_API_URL}/refunds",
        json={"charge_id": charge_id, "amount": amount},
        headers={"Authorization": f"Bearer {API_KEY}"},
    )
    return response.json()
""",
        },
        {
            "filename": "services/inventory_service.py",
            "language": "python",
            "content": """\
from typing import Dict, List

# In-memory store β€” lost on restart, not thread-safe
_inventory: Dict[str, int] = {}


def reserve_item(item_id: str, quantity: int) -> bool:
    # Race condition: check-then-act not atomic
    current = _inventory.get(item_id, 0)
    if current < quantity:
        return False
    _inventory[item_id] = current - quantity
    return True


def restock(item_id: str, quantity: int) -> None:
    # No validation β€” accepts negative quantity
    _inventory[item_id] = _inventory.get(item_id, 0) + quantity


def get_low_stock(threshold: int = 10) -> List[str]:
    # No pagination β€” could return millions of items
    return [k for k, v in _inventory.items() if v < threshold]


def bulk_reserve(items: List[Dict]) -> Dict[str, bool]:
    # Not atomic β€” partial reservation possible
    results = {}
    for item in items:
        results[item["id"]] = reserve_item(item["id"], item["qty"])
    return results
""",
        },
        {
            "filename": "api/router.py",
            "language": "python",
            "content": """\
from fastapi import FastAPI, HTTPException
from services import payment_service, inventory_service

app = FastAPI()
print("Starting API server")   # mixing print with logger


@app.post("/orders")
def create_order(order: dict):   # no Pydantic schema
    item_id    = order["item_id"]
    quantity   = order["quantity"]
    card_token = order["card_token"]
    amount     = order["amount"]

    reserved = inventory_service.reserve_item(item_id, quantity)
    if not reserved:
        raise HTTPException(status_code=409, detail="Out of stock")

    try:
        result = payment_service.charge_card(card_token, amount)
        return {"status": "ok", "charge": result}
    except Exception as e:
        # Inventory NOT released on payment failure β€” leaked reservation
        # Raw exception message returned to client
        raise HTTPException(status_code=500, detail=str(e))


@app.get("/health")
def health():
    # No dependency checks
    return {"status": "ok"}
""",
        },
        {
            "filename": "config/settings.py",
            "language": "python",
            "content": """\
# All config hardcoded β€” no env vars
DATABASE_URL   = "postgresql://admin:password123@localhost:5432/appdb"
REDIS_URL      = "redis://localhost:6379"
JWT_SECRET     = "my_jwt_secret_do_not_share"
LOG_LEVEL      = "DEBUG"   # leaks sensitive data in production
PAYMENT_API_KEY = "pk_live_abc123xyz"

# No pydantic BaseSettings, no dev/staging/prod distinction
""",
        },
    ],
    "ground_truth_issues": {
        "security": [
            "Hardcoded production API key in payment_service.py",
            "Hardcoded database credentials in settings.py",
            "JWT secret committed to source code",
            "DEBUG log level leaks sensitive data in production",
        ],
        "reliability": [
            "No retry logic on payment API calls",
            "No request timeout on external HTTP calls",
            "Race condition in inventory reserve_item",
            "Partial reservation in bulk_reserve β€” no atomicity",
            "Leaked inventory reservation on payment failure",
        ],
        "api_design": [
            "Generic dict input in create_order β€” no Pydantic schema",
            "Raw exception message returned to client",
            "Health endpoint has no dependency checks",
            "No idempotency key in refund_charge",
        ],
        "observability": [
            "print() mixed with logger β€” inconsistent logging",
            "No distributed tracing or correlation IDs",
            "No structured JSON log format",
        ],
        "scalability": [
            "In-memory inventory store β€” not distributed-safe",
            "get_low_stock has no pagination",
            "No connection pooling configured",
        ],
    },
}

EASY_SAMPLES   = [EASY_SAMPLE_1, EASY_SAMPLE_2]
MEDIUM_SAMPLES = [MEDIUM_SAMPLE_1]
HARD_SAMPLES   = [HARD_SAMPLE_1]