Spaces:
Running
Running
Major grading overhaul: difficulty multiplier, tighter scoring, mastery removal, precision penalties
72b3e8d | # server/datasets/security_cases.py | |
| # Ground truth cases for MCP Security Sandbox tasks. | |
| # | |
| # FIXES APPLIED: | |
| # 1. sec_easy: CVSS ranges made tighter (no more [7.5, 9.8] that any model guesses) | |
| # Also: done_conditions min_actions stays at 1 (easy IS easy), but threshold raised | |
| # 2. sec_medium: done_conditions required_sequence MUST include all 3 actions | |
| # Previously 'min_actions': 3 but required_sequence only had 2 — so it ended at step 2 | |
| # 3. sec_hard: same fix — required_sequence enforces all 3 steps | |
| # 4. required_fix_tokens: added more specific tokens so propose_fix is actually tested | |
| # 5. current_feedback_keywords: more specific so revise_fix is harder | |
| SECURITY_CASES = { | |
| 'sec_easy': [ | |
| { | |
| 'case_id': 'sec_easy_001', | |
| 'task_subtype': 'single', | |
| 'completion_threshold': 0.75, # FIX: was 0.80 — harder threshold | |
| 'max_steps': 4, | |
| # FIX: required_sequence is the done trigger. Easy only needs 1 action. | |
| 'done_conditions': {'min_actions': 1, 'required_sequence': ['identify_vulnerability']}, | |
| 'expected_vuln_type': 'sql_injection', | |
| 'cvss_range': [8.5, 9.3], # FIX: was [7.5, 9.8] — much tighter range | |
| 'expected_severity': 'critical', | |
| # FIX: More specific tokens — model must include ALL of these | |
| 'required_fix_tokens': ['?', 'execute', 'param', 'parameterized'], | |
| 'must_preserve_identifier': 'execute_query', | |
| 'current_feedback_keywords': ['sql', 'injection', 'union'], | |
| 'original_vuln_pattern': "query + user_input", | |
| 'tool_call': 'db.execute("SELECT * FROM users WHERE name = \'" + user_input + "\'")', | |
| 'task_description': 'Identify the SQL injection vulnerability in this MCP database tool call. Specify vuln_type, cvss_score (float), severity, and affected_line.', | |
| }, | |
| { | |
| 'case_id': 'sec_easy_002', | |
| 'task_subtype': 'single', | |
| 'completion_threshold': 0.75, | |
| 'max_steps': 4, | |
| 'done_conditions': {'min_actions': 1, 'required_sequence': ['identify_vulnerability']}, | |
| 'expected_vuln_type': 'xss', | |
| 'cvss_range': [4.8, 5.8], # FIX: very tight — model must know XSS CVSS precisely | |
| 'expected_severity': 'medium', | |
| 'required_fix_tokens': ['escape', 'html', 'sanitize'], | |
| 'must_preserve_identifier': 'render_html', | |
| 'current_feedback_keywords': ['xss', 'script', 'encoding'], | |
| 'original_vuln_pattern': '<script>', | |
| 'tool_call': 'render_html(f"<div>{user_comment}</div>")', | |
| 'task_description': 'Scan this HTML rendering call for XSS vulnerability. Specify vuln_type, cvss_score (float, 0.0-10.0), severity, and affected_line.', | |
| }, | |
| { | |
| 'case_id': 'sec_easy_003', | |
| 'task_subtype': 'single', | |
| 'completion_threshold': 0.75, | |
| 'max_steps': 4, | |
| 'done_conditions': {'min_actions': 1, 'required_sequence': ['identify_vulnerability']}, | |
| 'expected_vuln_type': 'hardcoded_secret', | |
| 'cvss_range': [6.2, 7.2], # FIX: tighter range | |
| 'expected_severity': 'high', | |
| 'required_fix_tokens': ['env', 'os', 'getenv', 'environ'], | |
| 'must_preserve_identifier': 'connect_db', | |
| 'current_feedback_keywords': ['hardcoded', 'password', 'secret', 'credential'], | |
| 'original_vuln_pattern': 'password="admin123"', | |
| 'tool_call': 'connect_db(host="prod-db.internal", password="admin123", user="root")', | |
| 'task_description': 'Scan this database connection call for hardcoded credentials. Specify vuln_type, cvss_score (float), severity, and affected_line.', | |
| }, | |
| { | |
| 'case_id': 'sec_easy_004', | |
| 'task_subtype': 'single', | |
| 'completion_threshold': 0.75, | |
| 'max_steps': 4, | |
| 'done_conditions': {'min_actions': 1, 'required_sequence': ['identify_vulnerability']}, | |
| 'expected_vuln_type': 'path_traversal', | |
| 'cvss_range': [6.8, 7.8], # FIX: tighter range | |
| 'expected_severity': 'high', | |
| 'required_fix_tokens': ['basename', 'path', 'normpath'], | |
| 'must_preserve_identifier': 'read_file', | |
| 'current_feedback_keywords': ['path', 'traversal', 'directory'], | |
| 'original_vuln_pattern': '../../../etc/passwd', | |
| 'tool_call': 'read_file(filepath=user_input) # user sends "../../etc/passwd"', | |
| 'task_description': 'Scan this file read call for path traversal vulnerability. Specify vuln_type, cvss_score (float), severity, and affected_line.', | |
| }, | |
| { | |
| 'case_id': 'sec_easy_005', | |
| 'task_subtype': 'single', | |
| 'completion_threshold': 0.75, | |
| 'max_steps': 4, | |
| 'done_conditions': {'min_actions': 1, 'required_sequence': ['identify_vulnerability']}, | |
| 'expected_vuln_type': 'missing_auth', | |
| 'cvss_range': [7.8, 8.8], # FIX: tighter range | |
| 'expected_severity': 'critical', | |
| 'required_fix_tokens': ['auth', 'token', 'verify', 'session'], | |
| 'must_preserve_identifier': 'delete_user', | |
| 'current_feedback_keywords': ['authentication', 'missing', 'unauthorized'], | |
| 'original_vuln_pattern': 'no auth check', | |
| 'tool_call': 'delete_user(user_id=request.params["id"]) # no authentication check', | |
| 'task_description': 'Scan this admin action call for missing authentication. Specify vuln_type, cvss_score (float), severity, and affected_line.', | |
| }, | |
| ], | |
| 'sec_medium': [ | |
| { | |
| 'case_id': 'sec_medium_001', | |
| 'task_subtype': 'multi', | |
| 'completion_threshold': 0.65, # FIX: was 0.75 — medium is harder to pass | |
| 'max_steps': 6, | |
| # FIX: required_sequence now has ALL 3 actions — episode won't end until all done | |
| 'done_conditions': {'min_actions': 3, 'required_sequence': ['identify_vulnerability', 'propose_fix', 'revise_fix']}, | |
| 'expected_vuln_type': 'sql_injection', | |
| 'cvss_range': [8.8, 9.5], # FIX: tighter range | |
| 'expected_severity': 'critical', | |
| # FIX: More specific fix tokens — model must use parameterized queries specifically | |
| 'required_fix_tokens': ['?', 'execute', 'param', 'parameterized', 'bind'], | |
| 'must_preserve_identifier': 'search_products', | |
| 'current_feedback_keywords': ['sql', 'union', 'injection', 'parameterize'], | |
| 'original_vuln_pattern': 'query + ', | |
| 'reviewer_feedback': 'Your fix prevents basic injection but UNION-based attacks are still possible. Use parameterized queries with bound parameters and add input length validation.', | |
| 'additional_vulns': [ | |
| {'vuln_type': 'xss', 'severity': 'medium', 'line': 15}, | |
| {'vuln_type': 'missing_auth', 'severity': 'high', 'line': 3}, | |
| ], | |
| 'tool_call': '''def search_products(query, user_id): | |
| # No auth check on line 3 | |
| results = db.execute("SELECT * FROM products WHERE name LIKE '%" + query + "%'") | |
| return f"<div>{results}</div>" # XSS on line 15''', | |
| 'task_description': 'This MCP tool has multiple security issues. Identify the most critical vulnerability, propose a fix, then revise based on reviewer feedback.', | |
| }, | |
| { | |
| 'case_id': 'sec_medium_002', | |
| 'task_subtype': 'multi', | |
| 'completion_threshold': 0.65, | |
| 'max_steps': 6, | |
| 'done_conditions': {'min_actions': 3, 'required_sequence': ['identify_vulnerability', 'propose_fix', 'revise_fix']}, | |
| 'expected_vuln_type': 'ssrf', | |
| 'cvss_range': [7.5, 8.5], # FIX: tighter | |
| 'expected_severity': 'high', | |
| 'required_fix_tokens': ['allowlist', 'url', 'private', 'block'], | |
| 'must_preserve_identifier': 'fetch_url', | |
| 'current_feedback_keywords': ['ssrf', 'internal', 'url', 'private', 'ip'], | |
| 'original_vuln_pattern': 'requests.get(user_url)', | |
| 'reviewer_feedback': 'Fix addresses external URLs but internal network requests (169.254.x.x, localhost) are still allowed. Add an allowlist or explicitly block private IP ranges.', | |
| 'additional_vulns': [ | |
| {'vuln_type': 'hardcoded_secret', 'severity': 'medium', 'line': 2}, | |
| ], | |
| 'tool_call': '''def fetch_url(user_url): | |
| API_KEY = "sk-secret-key-12345" # hardcoded secret | |
| resp = requests.get(user_url, headers={"Authorization": API_KEY}) | |
| return resp.text''', | |
| 'task_description': 'Scan this URL fetcher for vulnerabilities. Identify, propose a fix, then revise based on reviewer feedback.', | |
| }, | |
| { | |
| 'case_id': 'sec_medium_003', | |
| 'task_subtype': 'multi', | |
| 'completion_threshold': 0.65, | |
| 'max_steps': 6, | |
| 'done_conditions': {'min_actions': 3, 'required_sequence': ['identify_vulnerability', 'propose_fix', 'revise_fix']}, | |
| 'expected_vuln_type': 'idor', | |
| 'cvss_range': [6.5, 7.5], # FIX: tighter | |
| 'expected_severity': 'high', | |
| 'required_fix_tokens': ['user_id', 'owner', 'session', 'authorization'], | |
| 'must_preserve_identifier': 'get_document', | |
| 'current_feedback_keywords': ['idor', 'authorization', 'object', 'session'], | |
| 'original_vuln_pattern': 'doc_id from request', | |
| 'reviewer_feedback': 'Fix checks user ownership but does not validate the requesting user session. Add session-based auth check and compare session user_id with document owner_id.', | |
| 'additional_vulns': [ | |
| {'vuln_type': 'rate_limit_missing', 'severity': 'medium', 'line': 1}, | |
| ], | |
| 'tool_call': '''@app.get("/documents/{doc_id}") # no rate limiting | |
| def get_document(doc_id: int, request: Request): | |
| # No check if requesting user owns this document | |
| return db.documents.find_one({"_id": doc_id})''', | |
| 'task_description': 'Scan this document access endpoint for authorization issues. Identify, fix, then revise based on feedback.', | |
| }, | |
| ], | |
| 'sec_hard': [ | |
| { | |
| 'case_id': 'sec_hard_001', | |
| 'task_subtype': 'adversarial', | |
| 'completion_threshold': 0.60, # FIX: was 0.70 — hard IS hard | |
| 'max_steps': 8, | |
| 'done_conditions': {'min_actions': 3, 'required_sequence': ['identify_vulnerability', 'propose_fix', 'revise_fix']}, | |
| 'expected_vuln_type': 'sql_injection', | |
| 'cvss_range': [9.0, 9.8], # FIX: very tight — must score near-perfect on CVSS | |
| 'expected_severity': 'critical', | |
| # FIX: More demanding fix tokens — model must use all of these | |
| 'required_fix_tokens': ['?', 'execute', 'param', 'parameterized', 'validate', 'input'], | |
| 'must_preserve_identifier': 'send_email', | |
| 'current_feedback_keywords': ['still vulnerable', 'parameterize', 'concatenation', 'line 5'], | |
| 'original_vuln_pattern': "query + user_input", | |
| 'reviewer_feedback': 'Fix still allows SQL injection through the search parameter. Use parameterized queries for ALL string fields.', | |
| 'reviewer_feedback_sequence': [ | |
| 'Fix still allows SQL injection through the search parameter. Use parameterized queries for ALL string fields.', | |
| 'Better, but concatenation on line 5 still vulnerable. Also add input validation and length limits.', | |
| ], | |
| 'tool_call': '''def send_email(recipient, subject, body): | |
| query = "INSERT INTO emails (to_addr, subject, body) VALUES ('" + recipient + "', '" + subject + "', '" + body + "')" | |
| db.execute(query) | |
| log(f"Email sent to {recipient}") | |
| return {"status": "sent", "to": recipient}''', | |
| 'task_description': 'Rewrite this email tool call to remove SQL injection while preserving ALL business logic. A reviewer will evaluate your fix with adversarial inputs.', | |
| }, | |
| { | |
| 'case_id': 'sec_hard_002', | |
| 'task_subtype': 'adversarial', | |
| 'completion_threshold': 0.60, | |
| 'max_steps': 8, | |
| 'done_conditions': {'min_actions': 3, 'required_sequence': ['identify_vulnerability', 'propose_fix', 'revise_fix']}, | |
| 'expected_vuln_type': 'jwt_misuse', | |
| 'cvss_range': [8.5, 9.3], # FIX: tighter | |
| 'expected_severity': 'critical', | |
| # FIX: More demanding fix tokens | |
| 'required_fix_tokens': ['verify', 'algorithm', 'HS256', 'secret', 'algorithms'], | |
| 'must_preserve_identifier': 'authenticate_user', | |
| 'current_feedback_keywords': ['jwt', 'algorithm', 'none', 'verify', 'signature'], | |
| 'original_vuln_pattern': 'algorithms=["none"]', | |
| 'reviewer_feedback': 'Fix does not prevent algorithm=none attack. Explicitly set allowed_algorithms=["HS256"] and use server-side secret.', | |
| 'reviewer_feedback_sequence': [ | |
| 'Fix does not prevent algorithm=none attack. Explicitly set allowed_algorithms=["HS256"] and use server-side secret.', | |
| 'Algorithm is set but secret key is still derived from user input. Use a hardcoded server secret from environment variables.', | |
| ], | |
| 'tool_call': '''def authenticate_user(token): | |
| payload = jwt.decode(token, options={"verify_signature": False}) | |
| user_id = payload.get("user_id") | |
| return get_user(user_id)''', | |
| 'task_description': 'Rewrite this JWT authentication to prevent algorithm confusion attacks while preserving user lookup logic.', | |
| }, | |
| { | |
| 'case_id': 'sec_hard_003', | |
| 'task_subtype': 'adversarial', | |
| 'completion_threshold': 0.60, | |
| 'max_steps': 8, | |
| 'done_conditions': {'min_actions': 3, 'required_sequence': ['identify_vulnerability', 'propose_fix', 'revise_fix']}, | |
| 'expected_vuln_type': 'xxe', | |
| 'cvss_range': [7.8, 8.8], # FIX: tighter | |
| 'expected_severity': 'high', | |
| 'required_fix_tokens': ['disable', 'external', 'entities', 'dtd', 'defusedxml'], | |
| 'must_preserve_identifier': 'parse_xml', | |
| 'current_feedback_keywords': ['xxe', 'entity', 'external', 'dtd', 'defused'], | |
| 'original_vuln_pattern': 'allow_external_entities=True', | |
| 'reviewer_feedback': "Fix disables DTD but doesn't disable external entities. Set both no_network=True and forbid_dtd=True, or use defusedxml.", | |
| 'reviewer_feedback_sequence': [ | |
| "Fix disables DTD but doesn't disable external entities. Set both no_network=True and forbid_dtd=True.", | |
| 'Better, but you should also use defusedxml library for defense-in-depth and validate XML schema.', | |
| ], | |
| 'tool_call': '''def parse_xml(xml_string): | |
| import xml.etree.ElementTree as ET | |
| parser = ET.XMLParser() # Default parser allows XXE | |
| tree = ET.fromstring(xml_string, parser) | |
| return tree.findall('.//user/name')''', | |
| 'task_description': 'Fix the XXE vulnerability in this XML parser. Reviewer will test with external entity payloads.', | |
| }, | |
| { | |
| 'case_id': 'sec_hard_004', | |
| 'task_subtype': 'adversarial', | |
| 'completion_threshold': 0.60, | |
| 'max_steps': 8, | |
| 'done_conditions': {'min_actions': 3, 'required_sequence': ['identify_vulnerability', 'propose_fix', 'revise_fix']}, | |
| 'expected_vuln_type': 'ssrf', | |
| 'cvss_range': [8.0, 9.0], # FIX: tighter | |
| 'expected_severity': 'critical', | |
| 'required_fix_tokens': ['allowlist', 'private', 'ip', 'ipaddress', 'block'], | |
| 'must_preserve_identifier': 'fetch_webhook', | |
| 'current_feedback_keywords': ['ssrf', 'internal', 'network', 'private', 'ipv6'], | |
| 'original_vuln_pattern': 'requests.get(url)', | |
| 'reviewer_feedback': 'Fix blocks 127.0.0.1 but allows localhost and private IPs (10.x, 172.x, 192.168.x). Block ALL private ranges.', | |
| 'reviewer_feedback_sequence': [ | |
| 'Fix blocks 127.0.0.1 but allows localhost and private IPs (10.x, 172.x, 192.168.x). Block ALL private ranges.', | |
| 'Good, but you should also block IPv6 loopback (::1) and link-local addresses (fe80::).', | |
| ], | |
| 'tool_call': '''def fetch_webhook(url): | |
| import requests | |
| # Webhook URL from user input | |
| response = requests.get(url, timeout=10) | |
| return response.json()''', | |
| 'task_description': 'Prevent SSRF attacks on this webhook fetcher. Reviewer will try internal network access bypasses including IPv6.', | |
| }, | |
| { | |
| 'case_id': 'sec_hard_005', | |
| 'task_subtype': 'adversarial', | |
| 'completion_threshold': 0.60, | |
| 'max_steps': 8, | |
| 'done_conditions': {'min_actions': 3, 'required_sequence': ['identify_vulnerability', 'propose_fix', 'revise_fix']}, | |
| 'expected_vuln_type': 'idor', | |
| 'cvss_range': [7.0, 8.0], # FIX: tighter | |
| 'expected_severity': 'high', | |
| 'required_fix_tokens': ['owner', 'session', 'user_id', 'token', 'verify'], | |
| 'must_preserve_identifier': 'update_profile', | |
| 'current_feedback_keywords': ['idor', 'authorization', 'owner', 'session', 'cryptographic'], | |
| 'original_vuln_pattern': 'profile_id from request', | |
| 'reviewer_feedback': 'Fix checks profile ownership but uses user_id from request body (attacker-controlled). Use session token, not request body user_id.', | |
| 'reviewer_feedback_sequence': [ | |
| 'Fix checks profile ownership but uses user_id from request body (attacker-controlled). Use session token.', | |
| 'Better, but session validation is weak. Use cryptographic session tokens, not just user_id in cookie.', | |
| ], | |
| 'tool_call': '''@app.post("/profile/update") | |
| def update_profile(profile_id: int, user_id: int, data: dict): | |
| # user_id comes from request body (!) | |
| profile = db.profiles.find_one({"_id": profile_id}) | |
| profile.update(data) | |
| return {"status": "updated"}''', | |
| 'task_description': 'Fix IDOR vulnerability allowing users to edit others\' profiles. Reviewer will test horizontal privilege escalation.', | |
| }, | |
| ], | |
| } | |