Research Disclaimer
This tutorial is based on:
- Semgrep v1.55+ (SAST scanning)
- Bandit v1.7+ (Python security linter)
- CodeQL v2.15+ (GitHub Advanced Security)
- SonarQube v10.3+ (code quality & security)
- Academic research on AI code generation security (NYU 2023 study, Stanford 2024 study)
- OWASP Top 10 2021 vulnerability classifications
All code examples demonstrate production-grade security scanning integrated into CI/CD pipelines. Tested with GitHub Actions, GitLab CI, and Jenkins. Security recommendations follow OWASP and NIST guidelines.
Introduction
AI coding assistants (GitHub Copilot, ChatGPT, Claude Code) accelerate development but can introduce security vulnerabilities if not properly reviewed. Research shows AI-generated code contains vulnerabilities in 40% of suggestions involving security-sensitive operations.
This guide demonstrates production security workflows:
- Static analysis tools: Semgrep, Bandit, CodeQL integration
- Secure prompting: Strategies for safe AI code generation
- CI/CD integration: Automated security scanning workflows
- Manual review: Checklist for AI-generated code
- Real vulnerabilities: Examples from actual AI outputs
- Complete automation: Production-ready security pipelines
AI Code Generation Risks
| Risk Category | Impact | Mitigation |
|---|---|---|
| SQL Injection | High - Data breach | Prepared statements, ORM, input validation |
| XSS | Medium - Session hijacking | Output encoding, CSP headers |
| Hardcoded credentials | Critical - Unauthorized access | Secret management, env variables |
| Insecure deserialization | Critical - Remote code execution | Type validation, safe parsers |
| Path traversal | High - File access | Path sanitization, allowlists |
| Weak cryptography | Medium - Data exposure | Modern algorithms (AES-256, SHA-256) |
| Missing auth checks | Critical - Privilege escalation | Centralized auth middleware |
Prerequisites
Required Knowledge:
- Understanding of common web vulnerabilities (OWASP Top 10)
- Experience with CI/CD pipelines
- Familiarity with Python, JavaScript, or Java
- Basic understanding of static analysis tools
Required Tools:
# Python static analysis
pip install bandit==1.7.5 semgrep==1.55.0
# JavaScript/TypeScript security
npm install -g eslint eslint-plugin-security
# Code quality platform
docker pull sonarqube:10.3-community
# GitHub CodeQL CLI (for local scanning)
# Download from https://github.com/github/codeql-cli-binaries/releases
Example 1: SQL Injection in AI-Generated Code
Vulnerable Code (AI-Generated)
Prompt to AI: “Write a Python function to get user by email from database”
AI Output (Insecure):
def get_user_by_email(email):
"""Get user from database by email."""
import sqlite3
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
# VULNERABILITY: SQL Injection
query = f"SELECT * FROM users WHERE email = '{email}'"
cursor.execute(query)
user = cursor.fetchone()
conn.close()
return user
Exploitation:
# Attacker input
malicious_email = "' OR '1'='1"
get_user_by_email(malicious_email)
# Executes: SELECT * FROM users WHERE email = '' OR '1'='1'
# Returns ALL users
Secure Version
Secure Prompt: “Write a Python function to get user by email using parameterized query”
AI Output (Secure):
def get_user_by_email(email: str):
"""
Get user from database by email using parameterized query.
Args:
email: User email address (validated)
Returns:
User record or None
"""
import sqlite3
from email_validator import validate_email, EmailNotValidError
# Validate email format
try:
valid = validate_email(email)
email = valid.email
except EmailNotValidError:
return None
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
# SECURE: Parameterized query (prevents SQL injection)
query = "SELECT * FROM users WHERE email = ?"
cursor.execute(query, (email,))
user = cursor.fetchone()
conn.close()
return user
Static Analysis with Semgrep
Semgrep provides pattern-based vulnerability detection.
Step 1: Install and Configure
File: .semgrep.yml - Semgrep configuration
# Semgrep configuration for AI-generated code security
rules:
# Detect SQL injection patterns
- id: sql-injection-risk
patterns:
- pattern: |
cursor.execute(f"... {$VAR} ...")
- pattern: |
cursor.execute("... + $VAR + ...")
- pattern-not: |
cursor.execute("...", (...))
message: |
Potential SQL injection: Use parameterized queries instead of string formatting
languages: [python]
severity: ERROR
# Detect hardcoded credentials
- id: hardcoded-credentials
patterns:
- pattern-regex: '(password|api_key|secret|token)\s*=\s*["\'][^"\']+["\']'
- pattern-not-regex: '(password|api_key)\s*=\s*os\.(getenv|environ)'
message: |
Hardcoded credentials detected. Use environment variables instead.
languages: [python, javascript]
severity: ERROR
# Detect weak cryptography
- id: weak-crypto
patterns:
- pattern: hashlib.md5(...)
- pattern: hashlib.sha1(...)
message: |
Weak cryptographic hash detected. Use SHA-256 or stronger.
languages: [python]
severity: WARNING
# Detect missing input validation
- id: missing-input-validation
patterns:
- pattern: |
def $FUNC($PARAM):
...
open($PARAM, ...)
- pattern-not: |
def $FUNC($PARAM):
...
if ... in $PARAM: ...
...
open($PARAM, ...)
message: |
Path traversal risk: Validate file paths before opening
languages: [python]
severity: WARNING
# Detect eval() usage
- id: dangerous-eval
patterns:
- pattern: eval(...)
message: |
Dangerous eval() detected. This can lead to remote code execution.
languages: [python]
severity: ERROR
Step 2: Run Semgrep Scan
File: run_semgrep.sh
#!/bin/bash
# Run Semgrep security scan
set -e
echo "Running Semgrep security scan..."
# Run with custom rules
semgrep \
--config .semgrep.yml \
--config "p/security-audit" \
--config "p/owasp-top-ten" \
--config "p/python" \
--json \
--output semgrep-results.json \
.
# Generate human-readable report
semgrep \
--config .semgrep.yml \
--config "p/security-audit" \
.
# Check if critical issues found
CRITICAL_COUNT=$(jq '[.results[] | select(.extra.severity == "ERROR")] | length' semgrep-results.json)
if [ "$CRITICAL_COUNT" -gt 0 ]; then
echo "❌ Found $CRITICAL_COUNT critical security issues!"
exit 1
else
echo "✓ No critical security issues found"
fi
Python Security with Bandit
Bandit specializes in Python security issues.
Bandit Configuration
File: .bandit
[bandit]
# Exclude test and virtual environment directories
exclude_dirs = /tests,/venv,/.venv,/build
# Skip specific test IDs (if needed)
# skips = B404,B603
# Note: Most Bandit options are better configured via CLI flags:
# - Severity levels: --severity-level (low/medium/high)
# - Confidence: --confidence-level (low/medium/high)
# - Specific tests: -s B101,B601 (skip) or -t B201,B301 (run only)
File: run_bandit.py - Automated Bandit scanning
"""
Run Bandit security scan and generate report.
"""
import subprocess
import json
import sys
def run_bandit_scan(target_dir="."):
"""
Run Bandit security scan.
Args:
target_dir: Directory to scan
Returns:
Exit code (0 = pass, 1 = fail)
"""
# Run Bandit
cmd = [
"bandit",
"-r", target_dir,
"-f", "json",
"-o", "bandit-report.json",
"-c", ".bandit",
"--severity-level", "medium",
]
try:
subprocess.run(cmd, check=True)
except subprocess.CalledProcessError:
pass # Bandit returns non-zero when issues found
# Parse results
with open("bandit-report.json", "r") as f:
results = json.load(f)
# Categorize issues
critical_issues = [
issue for issue in results.get("results", [])
if issue["issue_severity"] in ["HIGH", "MEDIUM"]
]
# Print summary
print("\n" + "=" * 60)
print("Bandit Security Scan Results")
print("=" * 60)
print(f"Total issues: {len(results.get('results', []))}")
print(f"Critical/High issues: {len(critical_issues)}")
if critical_issues:
print("\nCritical Issues Found:")
for issue in critical_issues[:10]: # Show top 10
print(f"\n [{issue['issue_severity']}] {issue['issue_text']}")
print(f" File: {issue['filename']}:{issue['line_number']}")
print(f" CWE: {issue.get('issue_cwe', {}).get('id', 'N/A')}")
# Generate HTML report
subprocess.run([
"bandit",
"-r", target_dir,
"-f", "html",
"-o", "bandit-report.html"
])
print(f"\nHTML report: bandit-report.html")
# Fail if critical issues found
if len(critical_issues) > 0:
print("\n❌ Critical security issues detected!")
return 1
else:
print("\n✓ No critical security issues")
return 0
if __name__ == "__main__":
sys.exit(run_bandit_scan())
CI/CD Integration
GitHub Actions Workflow
File: .github/workflows/security-scan.yml
name: Security Scan
on:
pull_request:
branches: [main, develop]
push:
branches: [main]
jobs:
security-scan:
name: Run Security Scans
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for better analysis
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install security tools
run: |
pip install bandit semgrep
npm install -g eslint eslint-plugin-security
- name: Run Semgrep
run: |
semgrep --config=.semgrep.yml \
--config="p/security-audit" \
--config="p/owasp-top-ten" \
--sarif \
--output=semgrep.sarif \
.
continue-on-error: true
- name: Run Bandit
run: |
bandit -r . -f json -o bandit-report.json || true
python run_bandit.py
continue-on-error: true
- name: Upload Semgrep results to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: semgrep.sarif
if: always()
- name: Comment PR with results
uses: actions/github-script@v7
if: github.event_name == 'pull_request'
with:
script: |
const fs = require('fs');
const report = JSON.parse(fs.readFileSync('bandit-report.json', 'utf8'));
const criticalIssues = report.results.filter(
r => r.issue_severity === 'HIGH' || r.issue_severity === 'MEDIUM'
);
let comment = '## 🔐 Security Scan Results\n\n';
comment += `**Total Issues**: ${report.results.length}\n`;
comment += `**Critical/High**: ${criticalIssues.length}\n\n`;
if (criticalIssues.length > 0) {
comment += '### ⚠️ Critical Issues\n\n';
criticalIssues.slice(0, 5).forEach(issue => {
comment += `- **${issue.issue_text}**\n`;
comment += ` - File: \`${issue.filename}:${issue.line_number}\`\n`;
comment += ` - Severity: ${issue.issue_severity}\n\n`;
});
} else {
comment += '✅ No critical security issues found!\n';
}
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: comment
});
- name: Fail if critical issues
run: |
CRITICAL=$(jq '[.results[] | select(.issue_severity == "HIGH" or .issue_severity == "MEDIUM")] | length' bandit-report.json)
if [ "$CRITICAL" -gt 0 ]; then
echo "❌ $CRITICAL critical issues found"
exit 1
fi
GitLab CI Integration
File: .gitlab-ci.yml
stages:
- security
security_scan:
stage: security
image: python:3.11-slim
before_script:
- pip install bandit semgrep
script:
# Run Semgrep
- semgrep --config=.semgrep.yml --config="p/security-audit" --json -o semgrep.json .
# Run Bandit
- bandit -r . -f json -o bandit.json || true
# Check for critical issues
- |
CRITICAL=$(jq '[.results[] | select(.issue_severity == "HIGH")] | length' bandit.json)
if [ "$CRITICAL" -gt 0 ]; then
echo "Critical security issues found: $CRITICAL"
exit 1
fi
artifacts:
reports:
sast: semgrep.json
paths:
- bandit.json
- semgrep.json
expire_in: 30 days
only:
- merge_requests
- main
Secure AI Prompting Strategies
1. Specify Security Requirements
Bad Prompt:
Write a login endpoint
Good Prompt:
Write a secure login endpoint with:
- Bcrypt password hashing
- Rate limiting (5 attempts per minute)
- Input validation for email and password
- CSRF token validation
- Secure session cookies (httpOnly, sameSite)
- No hardcoded secrets
2. Request Security Comments
Prompt:
Write a file upload handler with security comments explaining:
- Why each validation check is needed
- What attacks are being prevented
- Which OWASP Top 10 items are addressed
3. Ask for Both Insecure and Secure Versions
Prompt:
Show me:
1. An insecure version of a password reset function
2. A secure version with explanations of what was fixed
3. Unit tests that verify the security fixes
Manual Code Review Checklist
File: ai-code-review-checklist.md
# AI-Generated Code Security Checklist
## Input Validation
- [ ] All user inputs are validated (type, length, format)
- [ ] File paths are sanitized (no `../` path traversal)
- [ ] SQL queries use parameterized statements
- [ ] Regular expressions are not vulnerable to ReDoS
- [ ] File uploads check file type and size
## Authentication & Authorization
- [ ] Authentication checks are present on protected endpoints
- [ ] Role-based access control (RBAC) is implemented
- [ ] Session tokens are generated securely (crypto.randomBytes)
- [ ] Passwords are hashed with bcrypt/argon2 (not MD5/SHA1)
- [ ] Rate limiting is applied to auth endpoints
## Cryptography
- [ ] No hardcoded secrets (passwords, API keys, tokens)
- [ ] Secrets loaded from environment variables
- [ ] Strong algorithms used (AES-256, SHA-256, not MD5/DES)
- [ ] Random values generated with crypto library (not Math.random)
- [ ] TLS/HTTPS enforced for sensitive data
## Data Protection
- [ ] Sensitive data not logged (passwords, credit cards, SSNs)
- [ ] Database connections use prepared statements
- [ ] No eval() or exec() calls
- [ ] Deserialization uses safe parsers (not pickle)
- [ ] Output is escaped before rendering (XSS prevention)
## Error Handling
- [ ] Errors don't leak sensitive information
- [ ] Stack traces not exposed to users
- [ ] Generic error messages returned to client
- [ ] Detailed errors logged server-side only
## Dependencies
- [ ] No known vulnerable dependencies (run `npm audit` or `pip-audit`)
- [ ] Dependencies pinned to specific versions
- [ ] Minimal dependencies (reduce attack surface)
## Configuration
- [ ] Debug mode disabled in production
- [ ] CORS configured properly (not `Access-Control-Allow-Origin: *`)
- [ ] Security headers set (CSP, X-Frame-Options, etc.)
- [ ] Default credentials changed
Real Vulnerability Examples
Example 1: Path Traversal
AI-Generated (Vulnerable):
@app.route('/download/<filename>')
def download_file(filename):
"""Download file from uploads directory."""
file_path = f"uploads/{filename}"
return send_file(file_path)
# EXPLOIT: /download/../../etc/passwd
# Accesses files outside uploads directory
Secure Version:
import os
from pathlib import Path
from flask import abort
@app.route('/download/<filename>')
def download_file(filename):
"""Securely download file from uploads directory."""
# Define allowed directory
uploads_dir = Path("/var/www/uploads").resolve()
# Build file path
file_path = (uploads_dir / filename).resolve()
# Verify file is within allowed directory
if not str(file_path).startswith(str(uploads_dir)):
abort(403, "Access denied")
# Verify file exists
if not file_path.is_file():
abort(404, "File not found")
return send_file(file_path)
Example 2: XSS (Cross-Site Scripting)
AI-Generated (Vulnerable):
// Display user comment
function showComment(comment) {
document.getElementById('comments').innerHTML += `
<div class="comment">${comment}</div>
`;
}
// EXPLOIT: comment = "<script>alert(document.cookie)</script>"
// Executes malicious JavaScript
Secure Version:
function showComment(comment) {
const commentDiv = document.createElement('div');
commentDiv.className = 'comment';
commentDiv.textContent = comment; // Automatically escapes HTML
document.getElementById('comments').appendChild(commentDiv);
}
// Or use a template library with auto-escaping (React, Vue, etc.)
Automated Security Testing
File: test_security.py - Security unit tests
"""
Security tests for AI-generated code.
"""
import pytest
from app import get_user_by_email, download_file
class TestSQLInjection:
"""Test SQL injection prevention."""
def test_sql_injection_blocked(self):
"""Verify SQL injection is blocked."""
malicious_input = "' OR '1'='1"
result = get_user_by_email(malicious_input)
# Should return None, not all users
assert result is None
def test_parameterized_query(self, monkeypatch):
"""Verify parameterized queries are used."""
import sqlite3
executed_queries = []
original_execute = sqlite3.Cursor.execute
def mock_execute(self, query, params=()):
executed_queries.append((query, params))
return original_execute(self, query, params)
monkeypatch.setattr(sqlite3.Cursor, "execute", mock_execute)
get_user_by_email("[email protected]")
# Verify parameterized query was used
assert len(executed_queries) > 0
query, params = executed_queries[0]
assert "?" in query # Placeholder
assert len(params) > 0 # Parameters provided
class TestPathTraversal:
"""Test path traversal prevention."""
def test_path_traversal_blocked(self, client):
"""Verify path traversal is blocked."""
response = client.get('/download/../../etc/passwd')
assert response.status_code == 403
def test_valid_file_allowed(self, client):
"""Verify valid files can be downloaded."""
response = client.get('/download/valid_file.txt')
assert response.status_code in [200, 404] # OK or not found
class TestXSS:
"""Test XSS prevention."""
def test_html_escaped(self, client):
"""Verify HTML is escaped in output."""
xss_payload = "<script>alert('XSS')</script>"
response = client.post('/comment', json={'text': xss_payload})
# Response should not contain executable script
assert b'<script>' not in response.data
assert b'<script>' in response.data or b'alert' not in response.data
Known Limitations
| Limitation | Impact | Mitigation |
|---|---|---|
| False positives | Wasted review time | Tune rules, suppress known safe patterns |
| False negatives | Missed vulnerabilities | Combine multiple tools, manual review |
| Language coverage | Some languages lack tools | Use CodeQL for broader support |
| Performance | Slow scans on large codebases | Scan only changed files in CI |
| Configuration complexity | Hard to set up | Use pre-built rulesets (p/security-audit) |
| AI prompt variability | Inconsistent security | Use prompt templates, prefix requirements |
Conclusion
Securing AI-generated code requires automated tooling, secure prompting, and manual review:
- Static Analysis: Semgrep, Bandit, CodeQL catch common vulnerabilities
- CI/CD Integration: Automated scanning on every PR
- Secure Prompting: Specify security requirements in prompts
- Manual Review: Use checklist for critical code
- Testing: Security-focused unit tests
Key Takeaways:
- Never trust AI-generated code without review
- Automate security scanning in CI/CD
- Use specific security requirements in prompts
- Combine multiple tools for better coverage
- Test security assumptions with unit tests
Next Steps:
- Integrate Semgrep/Bandit into your CI pipeline
- Create prompt templates for common secure patterns
- Build security test suite for AI-generated code
- Train team on common AI code vulnerabilities
Further Resources:
- Semgrep Rules: https://semgrep.dev/r
- Bandit Documentation: https://bandit.readthedocs.io/
- CodeQL: https://codeql.github.com/
- OWASP Top 10: https://owasp.org/Top10/
- AI Code Security Research: https://arxiv.org/abs/2108.09293