diff --git a/seed.py b/seed.py index 98a316d..0e7d756 100644 --- a/seed.py +++ b/seed.py @@ -289,7 +289,7 @@ def get_md5_hash(password: str): reviews = [ Review(product_id=p1.id, user_id=user.id, rating=5, comment="These guys are hilarious! They really do play dead.", is_approved=True), - Review(product_id=p1.id, user_id=hacker.id, rating=1, comment=" Boring.", is_approved=True), # V-003 XSS payload + Review(product_id=p1.id, user_id=hacker.id, rating=1, comment=" Boring.", is_approved=True), Review(product_id=p2.id, user_id=user.id, rating=4, comment="Strong pincers! Watch your fingers.", is_approved=True) ] db.add_all(reviews) diff --git a/src/auth.py b/src/auth.py index 0054fc7..4918193 100644 --- a/src/auth.py +++ b/src/auth.py @@ -2,15 +2,11 @@ from jose import jwt, JWTError import hashlib -# V-011: Weak secret hardcoded SECRET_KEY = "bugstore_secret_2024" ALGORITHM = "HS256" def create_access_token(data: dict): - """ - Generate JWT. - V-011: Weak configuration and potential for 'alg: none' (simulated by allowing varied algs if needed). - """ + """Generate JWT.""" to_encode = data.copy() expire = datetime.utcnow() + timedelta(days=1) # 24h as per F-007 to_encode.update({"exp": expire}) @@ -18,10 +14,7 @@ def create_access_token(data: dict): return encoded_jwt def decode_access_token(token: str): - """ - Decode JWT. - V-011: Insecure decoding - accepts weak algorithms. - """ + """Decode JWT.""" try: # Deliberately allowing 'none' algorithm if user specifies it? # Actually HS256 is the default but we can mock it. @@ -31,9 +24,7 @@ def decode_access_token(token: str): return None def get_password_hash(password: str): - """ - V-006: Insecure MD5 hashing. - """ + """Hash password for storage.""" return hashlib.md5(password.encode()).hexdigest() def verify_password(plain_password: str, hashed_password: str): @@ -52,9 +43,7 @@ def verify_password(plain_password: str, hashed_password: str): oauth2_scheme_optional = OAuth2PasswordBearer(tokenUrl="auth/login", auto_error=False) def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): - """ - V-011: Weak JWT validation. - """ + """Resolve the current user from the bearer token.""" payload = decode_access_token(token) if not payload: raise HTTPException( diff --git a/src/cart.py b/src/cart.py index e98bafb..2702758 100644 --- a/src/cart.py +++ b/src/cart.py @@ -2,10 +2,7 @@ from src.models import CartItem, Product, Coupon class CartManager: - """ - Handles shopping cart logic using session_id for persistence. - Deliberately maintains simplicity to allow for vulnerabilities V-023 and V-024 later. - """ + """Handles shopping cart logic using session_id for persistence.""" def __init__(self, db: Session, session_id: str): self.db = db self.session_id = session_id @@ -71,10 +68,7 @@ def clear(self): self.db.commit() def get_totals(self): - """ - Calculate totals. - Note: Frontend will try to override this in F-004 (V-023). - """ + """Calculate totals.""" items = self.get_items() subtotal = sum(item.product.price * item.quantity for item in items if item.product) tax = subtotal * 0.08 @@ -89,14 +83,10 @@ def get_totals(self): } def apply_coupon(self, code: str): - """ - Apply a discount code. - Deliberately vulnerable to V-024 (unlimited reuse/stacking logic flaws). - """ + """Apply a discount code.""" coupon = self.db.query(Coupon).filter( Coupon.code == code, Coupon.active == True ).first() - # We don't check if it was already applied in this session (V-024) return coupon diff --git a/src/email_service.py b/src/email_service.py index deaafaf..d84b035 100644 --- a/src/email_service.py +++ b/src/email_service.py @@ -3,18 +3,13 @@ from src.models import EmailTemplate def render_template(template_name: str, context: dict): - """ - Renders an email template by name. - V-027: Vulnerable to Server-Side Template Injection (SSTI). - """ + """Renders an email template by name.""" db = SessionLocal() try: template = db.query(EmailTemplate).filter(EmailTemplate.name == template_name).first() if not template: return None - # V-027: Using Jinja2 Template directly on user-controllable input (DB content) - # without sandbox. jinja_template = jinja2.Template(template.body) rendered_body = jinja_template.render(**context) diff --git a/src/frontend/index.html b/src/frontend/index.html index e183418..6431add 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -11,10 +11,8 @@
- - diff --git a/src/frontend/legacy/angular_widget.js b/src/frontend/legacy/angular_widget.js index d5ed1b4..a6b6278 100644 --- a/src/frontend/legacy/angular_widget.js +++ b/src/frontend/legacy/angular_widget.js @@ -1,20 +1,13 @@ /* - * V-004: AngularJS Template Injection (Legacy Widget) - * Uses vulnerable version 1.7.7 (deliberate). - * - * The CSTI works because legacy_q is injected DIRECTLY into the DOM - * before Angular bootstraps, so Angular compiles user input as a template. - * e.g. ?legacy_q={{constructor.constructor('alert(1)')()}} + * Legacy Angular blog search widget (AngularJS 1.7.7). */ (function () { if (typeof angular === 'undefined') return; - // Only activate on /blog routes if (!window.location.pathname.startsWith('/blog')) return; angular.module('legacyBugSearch', []); - // Wait for React to render, then inject the legacy widget var attempts = 0; var interval = setInterval(function () { attempts++; @@ -28,18 +21,14 @@ var legacyQ = urlParams.get('legacy_q'); if (!legacyQ) return; - // Create the legacy search widget container var widget = document.createElement('div'); widget.id = 'legacy-search-widget'; widget.style.cssText = 'background:#1a1333;border:1px solid #2d2255;border-radius:8px;padding:16px;margin:16px auto;max-width:800px;color:#e0d0ff;font-family:sans-serif;'; - // V-004: VULNERABLE — raw user input placed directly in Angular template HTML. - // Angular will compile this and evaluate any {{expressions}} inside legacyQ. widget.innerHTML = '
Legacy Search (AngularJS 1.7.7)
' + '
Results for: ' + legacyQ + '
'; - // Insert at the top of the page content var main = root.querySelector('main') || root.firstElementChild; if (main) { main.insertBefore(widget, main.firstChild); @@ -47,10 +36,8 @@ root.insertBefore(widget, root.firstChild); } - // Bootstrap Angular on this element — Angular will compile the template, - // evaluating any {{expressions}} that were in legacyQ angular.bootstrap(widget, ['legacyBugSearch']); - console.log('Legacy Angular widget initialized (V-004 active).'); + console.log('Legacy Angular widget initialized.'); }, 100); })(); diff --git a/src/frontend/legacy/custom.js b/src/frontend/legacy/custom.js index ecf258f..11aafe0 100644 --- a/src/frontend/legacy/custom.js +++ b/src/frontend/legacy/custom.js @@ -1,14 +1,11 @@ /* * BugStore Custom Client-Side Scripts - * Contains various vulnerabilities as per PRD F-031 */ -// V-007: Prototype Pollution via deepMerge function deepMerge(target, source) { for (let key in source) { if (source[key] && typeof source[key] === 'object') { if (!target[key]) target[key] = {}; - // V-007: No check for __proto__, constructor, prototype deepMerge(target[key], source[key]); } else { target[key] = source[key]; @@ -17,7 +14,6 @@ function deepMerge(target, source) { return target; } -// Parse URL filters and merge into config (V-007 trigger) function applyURLFilters() { const urlParams = new URLSearchParams(window.location.search); const filterParam = urlParams.get('filter'); @@ -34,19 +30,16 @@ function applyURLFilters() { } } -// V-003: DOM XSS via hash fragment function handleHashFragment() { const hash = window.location.hash.substring(1); if (hash) { const container = document.getElementById('hash-content'); if (container) { - // V-003: Unsafe innerHTML assignment container.innerHTML = decodeURIComponent(hash); } } } -// Newsletter subscription (CSRF vulnerable as per F-031) function subscribeNewsletter(email) { fetch('/api/newsletter/subscribe', { method: 'POST', @@ -58,13 +51,11 @@ function subscribeNewsletter(email) { .catch(err => console.error('Subscription failed:', err)); } -// Initialize on page load if (typeof window !== 'undefined') { window.addEventListener('DOMContentLoaded', function () { applyURLFilters(); handleHashFragment(); - // Setup newsletter form if exists const newsletterForm = document.getElementById('newsletter-form'); if (newsletterForm) { newsletterForm.addEventListener('submit', function (e) { @@ -77,7 +68,6 @@ if (typeof window !== 'undefined') { } }); - // Expose functions globally for testing window.BugStore = { deepMerge: deepMerge, subscribeNewsletter: subscribeNewsletter diff --git a/src/frontend/public/legacy/angular_widget.js b/src/frontend/public/legacy/angular_widget.js index d5ed1b4..a6b6278 100644 --- a/src/frontend/public/legacy/angular_widget.js +++ b/src/frontend/public/legacy/angular_widget.js @@ -1,20 +1,13 @@ /* - * V-004: AngularJS Template Injection (Legacy Widget) - * Uses vulnerable version 1.7.7 (deliberate). - * - * The CSTI works because legacy_q is injected DIRECTLY into the DOM - * before Angular bootstraps, so Angular compiles user input as a template. - * e.g. ?legacy_q={{constructor.constructor('alert(1)')()}} + * Legacy Angular blog search widget (AngularJS 1.7.7). */ (function () { if (typeof angular === 'undefined') return; - // Only activate on /blog routes if (!window.location.pathname.startsWith('/blog')) return; angular.module('legacyBugSearch', []); - // Wait for React to render, then inject the legacy widget var attempts = 0; var interval = setInterval(function () { attempts++; @@ -28,18 +21,14 @@ var legacyQ = urlParams.get('legacy_q'); if (!legacyQ) return; - // Create the legacy search widget container var widget = document.createElement('div'); widget.id = 'legacy-search-widget'; widget.style.cssText = 'background:#1a1333;border:1px solid #2d2255;border-radius:8px;padding:16px;margin:16px auto;max-width:800px;color:#e0d0ff;font-family:sans-serif;'; - // V-004: VULNERABLE — raw user input placed directly in Angular template HTML. - // Angular will compile this and evaluate any {{expressions}} inside legacyQ. widget.innerHTML = '
Legacy Search (AngularJS 1.7.7)
' + '
Results for: ' + legacyQ + '
'; - // Insert at the top of the page content var main = root.querySelector('main') || root.firstElementChild; if (main) { main.insertBefore(widget, main.firstChild); @@ -47,10 +36,8 @@ root.insertBefore(widget, root.firstChild); } - // Bootstrap Angular on this element — Angular will compile the template, - // evaluating any {{expressions}} that were in legacyQ angular.bootstrap(widget, ['legacyBugSearch']); - console.log('Legacy Angular widget initialized (V-004 active).'); + console.log('Legacy Angular widget initialized.'); }, 100); })(); diff --git a/src/frontend/public/legacy/custom.js b/src/frontend/public/legacy/custom.js index ecf258f..11aafe0 100644 --- a/src/frontend/public/legacy/custom.js +++ b/src/frontend/public/legacy/custom.js @@ -1,14 +1,11 @@ /* * BugStore Custom Client-Side Scripts - * Contains various vulnerabilities as per PRD F-031 */ -// V-007: Prototype Pollution via deepMerge function deepMerge(target, source) { for (let key in source) { if (source[key] && typeof source[key] === 'object') { if (!target[key]) target[key] = {}; - // V-007: No check for __proto__, constructor, prototype deepMerge(target[key], source[key]); } else { target[key] = source[key]; @@ -17,7 +14,6 @@ function deepMerge(target, source) { return target; } -// Parse URL filters and merge into config (V-007 trigger) function applyURLFilters() { const urlParams = new URLSearchParams(window.location.search); const filterParam = urlParams.get('filter'); @@ -34,19 +30,16 @@ function applyURLFilters() { } } -// V-003: DOM XSS via hash fragment function handleHashFragment() { const hash = window.location.hash.substring(1); if (hash) { const container = document.getElementById('hash-content'); if (container) { - // V-003: Unsafe innerHTML assignment container.innerHTML = decodeURIComponent(hash); } } } -// Newsletter subscription (CSRF vulnerable as per F-031) function subscribeNewsletter(email) { fetch('/api/newsletter/subscribe', { method: 'POST', @@ -58,13 +51,11 @@ function subscribeNewsletter(email) { .catch(err => console.error('Subscription failed:', err)); } -// Initialize on page load if (typeof window !== 'undefined') { window.addEventListener('DOMContentLoaded', function () { applyURLFilters(); handleHashFragment(); - // Setup newsletter form if exists const newsletterForm = document.getElementById('newsletter-form'); if (newsletterForm) { newsletterForm.addEventListener('submit', function (e) { @@ -77,7 +68,6 @@ if (typeof window !== 'undefined') { } }); - // Expose functions globally for testing window.BugStore = { deepMerge: deepMerge, subscribeNewsletter: subscribeNewsletter diff --git a/src/frontend/scripts/custom.js b/src/frontend/scripts/custom.js index ecf258f..11aafe0 100644 --- a/src/frontend/scripts/custom.js +++ b/src/frontend/scripts/custom.js @@ -1,14 +1,11 @@ /* * BugStore Custom Client-Side Scripts - * Contains various vulnerabilities as per PRD F-031 */ -// V-007: Prototype Pollution via deepMerge function deepMerge(target, source) { for (let key in source) { if (source[key] && typeof source[key] === 'object') { if (!target[key]) target[key] = {}; - // V-007: No check for __proto__, constructor, prototype deepMerge(target[key], source[key]); } else { target[key] = source[key]; @@ -17,7 +14,6 @@ function deepMerge(target, source) { return target; } -// Parse URL filters and merge into config (V-007 trigger) function applyURLFilters() { const urlParams = new URLSearchParams(window.location.search); const filterParam = urlParams.get('filter'); @@ -34,19 +30,16 @@ function applyURLFilters() { } } -// V-003: DOM XSS via hash fragment function handleHashFragment() { const hash = window.location.hash.substring(1); if (hash) { const container = document.getElementById('hash-content'); if (container) { - // V-003: Unsafe innerHTML assignment container.innerHTML = decodeURIComponent(hash); } } } -// Newsletter subscription (CSRF vulnerable as per F-031) function subscribeNewsletter(email) { fetch('/api/newsletter/subscribe', { method: 'POST', @@ -58,13 +51,11 @@ function subscribeNewsletter(email) { .catch(err => console.error('Subscription failed:', err)); } -// Initialize on page load if (typeof window !== 'undefined') { window.addEventListener('DOMContentLoaded', function () { applyURLFilters(); handleHashFragment(); - // Setup newsletter form if exists const newsletterForm = document.getElementById('newsletter-form'); if (newsletterForm) { newsletterForm.addEventListener('submit', function (e) { @@ -77,7 +68,6 @@ if (typeof window !== 'undefined') { } }); - // Expose functions globally for testing window.BugStore = { deepMerge: deepMerge, subscribeNewsletter: subscribeNewsletter diff --git a/src/frontend/src/components/ProductList.jsx b/src/frontend/src/components/ProductList.jsx index 318c26d..30a8f62 100644 --- a/src/frontend/src/components/ProductList.jsx +++ b/src/frontend/src/components/ProductList.jsx @@ -6,7 +6,6 @@ const ProductList = () => { const [searchTerm, setSearchTerm] = useState(''); useEffect(() => { - // Check URL for search param (V-002: Reflected XSS) const params = new URLSearchParams(window.location.search); const s = params.get('search'); if (s) { @@ -39,7 +38,6 @@ const ProductList = () => { className="font-bold underline decoration-coral ml-2" dangerouslySetInnerHTML={{ __html: searchTerm }} > - {/* V-002: Reflected XSS vulnerability planted here */} )} diff --git a/src/frontend/src/components/Reviews.jsx b/src/frontend/src/components/Reviews.jsx index 8eca2f9..47f4024 100644 --- a/src/frontend/src/components/Reviews.jsx +++ b/src/frontend/src/components/Reviews.jsx @@ -44,8 +44,6 @@ const Reviews = ({ productId }) => { product_id: parseInt(productId), rating: rating, comment: comment, - // V-020: Planted vulnerability - we allow the client to set is_approved - // An attacker can set this to true to bypass moderation. is_approved: false }) }); @@ -166,7 +164,6 @@ const Reviews = ({ productId }) => {
diff --git a/src/frontend/src/pages/AdminDashboard.jsx b/src/frontend/src/pages/AdminDashboard.jsx index eb9aa86..1281102 100644 --- a/src/frontend/src/pages/AdminDashboard.jsx +++ b/src/frontend/src/pages/AdminDashboard.jsx @@ -156,7 +156,7 @@ const AdminDashboard = () => { Security Advisory

- Warning: Debug endpoints currently exposed in sector 7-A (V-012). Maintain low observability metrics during maintenance. + Warning: Debug endpoints currently exposed in sector 7-A. Maintain low observability metrics during maintenance.

Leak Investigation Access diff --git a/src/frontend/src/pages/AdminProducts.jsx b/src/frontend/src/pages/AdminProducts.jsx index 8470dee..627420b 100644 --- a/src/frontend/src/pages/AdminProducts.jsx +++ b/src/frontend/src/pages/AdminProducts.jsx @@ -57,9 +57,6 @@ const AdminProducts = () => { const url = isNew ? '/api/admin/products' : `/api/admin/products/${editingProduct.id}`; const method = isNew ? 'POST' : 'PUT'; - // V-019: No client-side validation on these fields either. - // An attacker (or malicious admin) can inject scripts into description/name. - try { const res = await fetch(url, { method: method, diff --git a/src/frontend/src/pages/BlogDetail.jsx b/src/frontend/src/pages/BlogDetail.jsx index 2fcc04b..8bb8772 100644 --- a/src/frontend/src/pages/BlogDetail.jsx +++ b/src/frontend/src/pages/BlogDetail.jsx @@ -80,7 +80,6 @@ const BlogDetail = () => {
diff --git a/src/frontend/src/pages/Catalog.jsx b/src/frontend/src/pages/Catalog.jsx index ba05ae6..ac052c2 100644 --- a/src/frontend/src/pages/Catalog.jsx +++ b/src/frontend/src/pages/Catalog.jsx @@ -97,7 +97,6 @@ const Catalog = () => { dangerouslySetInnerHTML={{ __html: searchTerm }} >" - {/* V-002: Reflected XSS vulnerability planted here */} )} diff --git a/src/frontend/src/pages/Checkout.jsx b/src/frontend/src/pages/Checkout.jsx index cc18076..a53715e 100644 --- a/src/frontend/src/pages/Checkout.jsx +++ b/src/frontend/src/pages/Checkout.jsx @@ -39,7 +39,6 @@ const Checkout = () => { body: JSON.stringify({ shipping_address: address, payment_simulated: payment, - // V-023: Trusted Client Total - we send exactly what the frontend calculates total: cart.totals.total }) }); diff --git a/src/frontend/src/pages/OrderHistory.jsx b/src/frontend/src/pages/OrderHistory.jsx index 5231505..3f635f8 100644 --- a/src/frontend/src/pages/OrderHistory.jsx +++ b/src/frontend/src/pages/OrderHistory.jsx @@ -131,7 +131,7 @@ const OrderHistory = () => { to={`/orders/${order.id}`} className="text-xs font-black text-hive-muted underline flex items-center gap-1 hover:text-coral transition-colors" > - Public Tracking Link (V-009) + Public Tracking Link diff --git a/src/frontend/src/pages/ThreadDetail.jsx b/src/frontend/src/pages/ThreadDetail.jsx index 07b913f..2d18c41 100644 --- a/src/frontend/src/pages/ThreadDetail.jsx +++ b/src/frontend/src/pages/ThreadDetail.jsx @@ -13,8 +13,6 @@ const ThreadDetail = () => { const fetchThread = async () => { try { - // V-001: The {id} here is passed directly to the backend RAW SQL query. - // An attacker can browse /forum/thread/1 OR 1-- etc. const res = await fetch(`/api/forum/threads/${id}`); const data = await res.json(); if (res.ok) { diff --git a/src/graphql_server.py b/src/graphql_server.py index 74365a6..b524e17 100644 --- a/src/graphql_server.py +++ b/src/graphql_server.py @@ -28,12 +28,10 @@ class Order: total: float tracking_number: Optional[str] -# Queries (V-020: Information Disclosure) @strawberry.type class Query: @strawberry.field def users(self) -> List[User]: - # V-020: No pagination limits, no auth check. Enumeration of all users. db = SessionLocal() try: users = db.query(UserModel).all() @@ -51,7 +49,6 @@ def users(self) -> List[User]: @strawberry.field def orders(self, user_id: Optional[int] = None) -> List[Order]: - # V-020: Access any user's orders if user_id is provided, or ALL orders if not. db = SessionLocal() try: if user_id: @@ -81,12 +78,10 @@ def products(self) -> List[Product]: finally: db.close() -# Mutations (V-020: Broken Access Control) @strawberry.type class Mutation: @strawberry.mutation def update_bio(self, user_id: int, bio: str) -> str: - # V-020: IDOR. Can update any user's bio without auth check. db = SessionLocal() try: user = db.query(UserModel).filter(UserModel.id == user_id).first() diff --git a/src/importers/scanner_importers.py b/src/importers/scanner_importers.py index 13bfad7..11dfe22 100644 --- a/src/importers/scanner_importers.py +++ b/src/importers/scanner_importers.py @@ -19,7 +19,7 @@ class Finding: """Standardized vulnerability finding""" scanner: str - vuln_id: str = None # Mapped to V-001, V-002, etc. + vuln_id: str = None name: str = "" severity: str = "" url: str = "" @@ -152,7 +152,7 @@ def parse(json_file: str) -> List[Finding]: for vuln in data.get('vulnerabilities', []): finding = Finding( scanner='BugTraceAI', - vuln_id=vuln.get('id'), # Already mapped to V-XXX + vuln_id=vuln.get('id'), name=vuln.get('name', ''), severity=vuln.get('severity', ''), url=vuln.get('url', ''), @@ -267,7 +267,7 @@ def add_findings(self, findings: List[Finding]): def get_detection_rate(self) -> float: """Calculate detection rate (28 total vulnerabilities)""" - total_vulns = 28 # V-015 and V-016 not implemented + total_vulns = 28 detected = len(self.mapped_vulns) return (detected / total_vulns) * 100 diff --git a/src/main.py b/src/main.py index 7ceb7aa..b00da1e 100644 --- a/src/main.py +++ b/src/main.py @@ -37,10 +37,8 @@ async def __call__(self, scope, receive, send): REQUEST_TIMEOUT = float(os.getenv("BUGSTORE_REQUEST_TIMEOUT", "10")) app.add_middleware(TimeoutMiddleware, timeout=REQUEST_TIMEOUT) -# Difficulty Middleware (V-022) app.add_middleware(DifficultyMiddleware) -# CORS setup (V-008: Overly permissive CORS) app.add_middleware( CORSMiddleware, allow_origins=["*"], @@ -74,7 +72,6 @@ async def get_public_config(): "difficulty_level": int(os.getenv("BUGSTORE_DIFFICULTY", "0")), } -# GraphQL (V-020) from src.graphql_server import graphql_app app.include_router(graphql_app, prefix="/api/graphql") @@ -90,17 +87,13 @@ async def get_public_config(): @app.get("/") async def read_root(request: Request, response: Response, db: Session = Depends(get_db)): - """ - Root endpoint - serves frontend HTML. - V-013: SQL Injection via TrackingId cookie. - """ + """Root endpoint - serves frontend HTML.""" tracking_id = request.cookies.get("TrackingId") if not tracking_id: raw_id = str(uuid.uuid4()) tracking_id = base64.b64encode(raw_id.encode()).decode() response.set_cookie(key="TrackingId", value=tracking_id) - # V-013: Vulnerable SQL injection (blind — result not shown to user) try: decoded_id = base64.b64decode(tracking_id).decode() query = f"SELECT 1 FROM products WHERE description = '{decoded_id}' LIMIT 1" diff --git a/src/models.py b/src/models.py index bb9ae68..5f5c075 100644 --- a/src/models.py +++ b/src/models.py @@ -5,11 +5,7 @@ Base = declarative_base() class User(Base): - """ - User model for the colony members. - V-006: Storing passwords using MD5 (insecure hashing). - V-019: No validation on username allows for special characters. - """ + """User model for the colony members.""" __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) @@ -96,11 +92,7 @@ class Coupon(Base): active = Column(Boolean, default=True) class Order(Base): - """ - Customer orders. - V-009: Insecure direct object reference (IDOR) will be possible. - V-023: Trusting client totals. - """ + """Customer orders.""" __tablename__ = "orders" id = Column(Integer, primary_key=True, index=True) @@ -134,15 +126,12 @@ class OrderItem(Base): product = relationship("Product") class Blog(Base): - """ - Blog posts for the community hub. - V-016: Stored XSS vulnerability will be planted in content. - """ + """Blog posts for the community hub.""" __tablename__ = "blogs" id = Column(Integer, primary_key=True, index=True) title = Column(String(255), nullable=False) - content = Column(Text, nullable=False) # Stored XSS (V-016) + content = Column(Text, nullable=False) author_id = Column(Integer, ForeignKey("users.id")) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) @@ -150,11 +139,7 @@ class Blog(Base): author = relationship("User") class Review(Base): - """ - Product reviews by colony members. - V-020: Review moderation bypass potential. - V-003: Potential XSS in comments. - """ + """Product reviews by colony members.""" __tablename__ = "reviews" id = Column(Integer, primary_key=True, index=True) @@ -169,10 +154,7 @@ class Review(Base): user = relationship("User") class Thread(Base): - """ - Forum threads for colony discussions. - V-001: SQL injection target (simulated in routes). - """ + """Forum threads for colony discussions.""" __tablename__ = "threads" id = Column(Integer, primary_key=True, index=True) @@ -200,14 +182,11 @@ class Reply(Base): author = relationship("User") class EmailTemplate(Base): - """ - Email templates for notifications. - V-027: SSTI vulnerability in template rendering. - """ + """Email templates for notifications.""" __tablename__ = "email_templates" id = Column(Integer, primary_key=True, index=True) name = Column(String(100), unique=True, index=True, nullable=False) subject = Column(String(255), nullable=False) - body = Column(Text, nullable=False) # SSTI payload here + body = Column(Text, nullable=False) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) diff --git a/src/routes/admin.py b/src/routes/admin.py index 9c89163..1bd210d 100644 --- a/src/routes/admin.py +++ b/src/routes/admin.py @@ -54,11 +54,7 @@ def list_all_users( @router.get("/vulnerable-debug-stats") def get_vulnerable_stats(db: Session = Depends(get_db)): - """ - V-012: Broken Access Control. - A debug endpoint that returns sensitive stats without auth. - Useful for attackers to gauge target size. - """ + """Debug endpoint that returns sensitive stats without auth.""" return { "db_size_estimate": "Large", "active_sessions": 42, @@ -67,7 +63,7 @@ def get_vulnerable_stats(db: Session = Depends(get_db)): # Product Management (CRUD) class ProductBase(BaseModel): - name: str # V-019: No validation on length or characters (XSS/clash potential) + name: str species: str latin_name: str price: float @@ -131,7 +127,6 @@ def delete_product( db.commit() return {"message": "Specimen successfully retired from the colony."} -# Email Template Management (V-027 SSTI) class TemplateUpdate(BaseModel): subject: str body: str @@ -162,10 +157,7 @@ def preview_email_template( data: TemplateUpdate, current_user: User = Depends(check_role(["admin"])) ): - """ - Live preview of email template. - V-027: User input 'data.body' is rendered with Jinja2. - """ + """Live preview of email template.""" # Mock context for preview preview_context = { "user": {"name": "Larva Tester", "email": "test@bugstore.com"}, diff --git a/src/routes/auth.py b/src/routes/auth.py index 299435f..ebf870f 100644 --- a/src/routes/auth.py +++ b/src/routes/auth.py @@ -20,10 +20,7 @@ class UserLogin(BaseModel): @router.post("/register") def register(data: UserRegister, db: Session = Depends(get_db)): - """ - User registration. - V-019: No validation on username (special characters allowed). - """ + """User registration.""" # Check if user exists existing_user = db.query(User).filter( (User.username == data.username) | (User.email == data.email) @@ -34,9 +31,9 @@ def register(data: UserRegister, db: Session = Depends(get_db)): # Create new user new_user = User( - username=data.username, # No validation (V-019) + username=data.username, email=data.email, - password_hash=get_password_hash(data.password), # MD5 (V-006) + password_hash=get_password_hash(data.password), name=data.name, role="user" ) @@ -61,10 +58,7 @@ def register(data: UserRegister, db: Session = Depends(get_db)): @router.post("/login") def login(data: UserLogin, db: Session = Depends(get_db)): - """ - User login. - V-007: Plaintext response of JWT. - """ + """User login.""" user = db.query(User).filter(User.username == data.username).first() if not user or not verify_password(data.password, user.password_hash): diff --git a/src/routes/blog.py b/src/routes/blog.py index b55ae65..1a167af 100644 --- a/src/routes/blog.py +++ b/src/routes/blog.py @@ -35,14 +35,10 @@ def create_blog( db: Session = Depends(get_db), current_user: User = Depends(check_role(["staff", "admin"])) ): - """ - Create a blog post. - V-016: Stored XSS vulnerability. - We do NOT sanitize the 'content' field before saving. - """ + """Create a blog post.""" new_blog = Blog( title=data.title, - content=data.content, # V-016 + content=data.content, author_id=current_user.id ) db.add(new_blog) diff --git a/src/routes/cart.py b/src/routes/cart.py index da20654..34f73e4 100644 --- a/src/routes/cart.py +++ b/src/routes/cart.py @@ -15,10 +15,7 @@ class CouponApply(BaseModel): code: str def get_session_id(request: Request, response: Response): - """ - Retrieves or creates a session_id. - V-008: Cookie is not HttpOnly, not Secure, no SameSite. - """ + """Retrieves or creates a session_id.""" session_id = request.cookies.get("session_id") if not session_id: session_id = str(uuid.uuid4()) diff --git a/src/routes/catalog.py b/src/routes/catalog.py index d14fd72..26afa18 100644 --- a/src/routes/catalog.py +++ b/src/routes/catalog.py @@ -19,20 +19,15 @@ def get_products( offset: int = 0, db: Session = Depends(get_db) ): - """ - Get product list with vulnerable filtering (V-001, V-012). - """ + """Get product list with filtering.""" query_str = "SELECT * FROM products WHERE 1=1" - # V-001: SQL Injection via category (string concatenation) if category: query_str += f" AND category = '{category}'" - # V-001: SQL Injection via search (string concatenation) if search: query_str += f" AND name LIKE '%{search}%'" - # V-012: Blind SQL Injection via price filters (lack of type enforcement in query string) if min_price is not None: query_str += f" AND price >= {min_price}" if max_price is not None: @@ -72,17 +67,13 @@ def get_product(id: int, db: Session = Depends(get_db)): @router.get("/{id}/image") def get_product_image(id: int, file: str = Query(...)): - """ - Serve product image with Path Traversal vulnerability (V-014). - """ + """Serve product image.""" # Resolve static directory path relative to this file # src/routes/catalog.py -> src -> root current_dir = os.path.dirname(os.path.abspath(__file__)) root_dir = os.path.dirname(os.path.dirname(current_dir)) static_images_dir = os.path.join(root_dir, "static", "images") - # V-014: Path Traversal via string concatenation - # An attacker can use ?file=../../etc/passwd or similar image_path = os.path.join(static_images_dir, file) # In a real exploit, this would return any file the process can read. diff --git a/src/routes/checkout.py b/src/routes/checkout.py index 734631f..f618b9c 100644 --- a/src/routes/checkout.py +++ b/src/routes/checkout.py @@ -19,7 +19,6 @@ class Address(BaseModel): class CheckoutRequest(BaseModel): shipping_address: Address payment_simulated: Optional[dict] = None - # V-023: Trusted Client Total (frontend sends total to backend) total: float @router.post("/process") @@ -28,19 +27,14 @@ def process_checkout( db: Session = Depends(get_db), session_id: str = Depends(get_session_id) ): - """ - Process checkout. - V-023: Vulnerability planted - we trust the total sent from the client. - """ + """Process checkout.""" cart_items = db.query(CartItem).filter(CartItem.session_id == session_id).all() if not cart_items: raise HTTPException(status_code=400, detail="Your colony is empty. Cannot checkout.") - # Create the order - # V-023: Use the total from the request instead of calculating it on backend new_order = Order( - total=data.total, # TRUSTING THE CLIENT (V-023) + total=data.total, shipping_address=json.dumps(data.shipping_address.model_dump()), status="Pending" ) @@ -57,9 +51,6 @@ def process_checkout( ) db.add(order_item) - # V-025: No rate limiting or stock check? - # Simulating order confirmation - # Empty cart after checkout db.query(CartItem).filter(CartItem.session_id == session_id).delete() diff --git a/src/routes/forum.py b/src/routes/forum.py index da0ece0..21f1b7b 100644 --- a/src/routes/forum.py +++ b/src/routes/forum.py @@ -18,12 +18,8 @@ class ReplyCreate(BaseModel): @router.get("/threads") def get_threads(q: Optional[str] = None, db: Session = Depends(get_db)): - """ - List forum threads. - V-001: SQL Injection vulnerability in search query. - """ + """List forum threads.""" if q: - # V-001: RAW SQL with string formatting (DANGEROUS) query = f"SELECT * FROM threads WHERE title LIKE '%{q}%' OR content LIKE '%{q}%'" result = db.execute(sqlalchemy_text(query)) return [dict(row._mapping) for row in result] @@ -32,11 +28,7 @@ def get_threads(q: Optional[str] = None, db: Session = Depends(get_db)): @router.get("/threads/{thread_id}") def get_thread_detail(thread_id: str, db: Session = Depends(get_db)): - """ - Get thread details and replies. - V-001: SQL Injection vulnerability in thread_id parameter. - """ - # V-001: RAW SQL with string formatting (DANGEROUS) + """Get thread details and replies.""" query = f"SELECT * FROM threads WHERE id = {thread_id}" try: thread_result = db.execute(sqlalchemy_text(query)).first() diff --git a/src/routes/health.py b/src/routes/health.py index 42bb6e1..46ef406 100644 --- a/src/routes/health.py +++ b/src/routes/health.py @@ -12,7 +12,6 @@ async def health_check( ): """ System health check. - V-021: Remote Code Execution (RCE) via 'cmd' parameter. Only accessible as admin if 'cmd' is provided. """ response = { @@ -29,8 +28,6 @@ async def health_check( raise HTTPException(status_code=403, detail="Unauthorized execution.") try: - # SECURITY WARNING: This is a deliberate RCE vulnerability (V-021). - # Do not use this pattern in production code. output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) response["cmd_output"] = output.decode() except subprocess.CalledProcessError as e: diff --git a/src/routes/orders.py b/src/routes/orders.py index 1488ed7..ed283a1 100644 --- a/src/routes/orders.py +++ b/src/routes/orders.py @@ -8,10 +8,7 @@ @router.get("/") def get_orders(user_id: Optional[int] = None, db: Session = Depends(get_db)): - """ - Get all orders. - V-009: IDOR vulnerability - no access control. - """ + """Get all orders.""" if user_id: orders = db.query(Order).filter(Order.user_id == user_id).all() else: @@ -30,11 +27,7 @@ def get_orders(user_id: Optional[int] = None, db: Session = Depends(get_db)): @router.get("/{order_id}") def get_order_detail(order_id: int, db: Session = Depends(get_db)): - """ - Get order details by ID. - V-009: Direct Object Reference without authorization. - An attacker can guess order IDs. - """ + """Get order details by ID.""" order = db.query(Order).filter(Order.id == order_id).first() if not order: raise HTTPException(status_code=404, detail="Order not found in the swarm") diff --git a/src/routes/redirect.py b/src/routes/redirect.py index f8d612a..6c3e2dd 100644 --- a/src/routes/redirect.py +++ b/src/routes/redirect.py @@ -7,9 +7,7 @@ def redirect_to(url: str = Query(..., description="Destination URL")): """ Redirects user to the specified URL. - V-005, V-006: Open Redirect vulnerability. - No validation allows phishing redirects (http://evil.com) + No validation allows phishing redirects (http://evil.com) or potentially dangerous schemes (javascript:). """ - # V-005: Unvalidated Redirect return RedirectResponse(url=url) diff --git a/src/routes/review.py b/src/routes/review.py index 5d11b2d..c714a9f 100644 --- a/src/routes/review.py +++ b/src/routes/review.py @@ -12,8 +12,6 @@ class ReviewCreate(BaseModel): product_id: int rating: int comment: str - # V-020: Mass Assignment potential. If the user sends is_approved=True, - # and the backend blindly spreads the dict, it bypasses moderation. is_approved: bool = False @router.get("/{product_id}") @@ -32,17 +30,14 @@ def create_review( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): - """ - Submit a review. - V-020: Mass Assignment vulnerability. - """ + """Submit a review.""" # Blindly using data from client (simulating mass assignment) new_review = Review( product_id=data.product_id, user_id=current_user.id, rating=data.rating, comment=data.comment, - is_approved=data.is_approved # V-020: TRUSTING CLIENT on moderation flag + is_approved=data.is_approved ) db.add(new_review) diff --git a/src/routes/secure_portal.py b/src/routes/secure_portal.py index 39f6468..1ea8b18 100644 --- a/src/routes/secure_portal.py +++ b/src/routes/secure_portal.py @@ -3,10 +3,6 @@ Coexists with /admin (no 2FA) for testing both authentication flows. -Vulnerabilities planted: -- V-031: No rate-limit on TOTP verification (brute force possible) -- V-032: TOTP secret exposed in login response (info disclosure) - Original contribution by Neorichi (RSanchez), adapted for MariaDB codebase. """ @@ -62,10 +58,7 @@ def generate_totp_secret() -> str: def verify_totp(secret: str, code: str) -> bool: - """ - Verify a TOTP code. - V-031: NO rate limiting - allows brute force of 6-digit codes. - """ + """Verify a TOTP code.""" totp = pyotp.TOTP(secret) return totp.verify(code) @@ -124,12 +117,7 @@ def role_verifier(current_user: User = Depends(get_current_user_2fa)): @router.post("/login") def secure_login(data: SecureLoginRequest, db: Session = Depends(get_db)): - """ - Login for secure-portal. Requires username, password AND TOTP code. - - V-031: No rate-limit on TOTP verification (brute force possible). - V-032: TOTP secret exposed in response (info disclosure). - """ + """Login for secure-portal. Requires username, password AND TOTP code.""" user = db.query(User).filter(User.username == data.username).first() if not user or not verify_password(data.password, user.password_hash): @@ -144,7 +132,6 @@ def secure_login(data: SecureLoginRequest, db: Session = Depends(get_db)): detail="2FA not configured. Please set up TOTP first at /secure-portal/setup" ) - # V-031: NO rate limiting here - allows brute force of TOTP code if not verify_totp(user.totp_secret, data.totp_code): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -157,7 +144,6 @@ def secure_login(data: SecureLoginRequest, db: Session = Depends(get_db)): "role": user.role }) - # V-032: Expose totp_secret in response (info disclosure) return { "access_token": token, "token_type": "bearer", @@ -165,7 +151,7 @@ def secure_login(data: SecureLoginRequest, db: Session = Depends(get_db)): "id": user.id, "username": user.username, "role": user.role, - "totp_secret": user.totp_secret # V-032: Should not be here + "totp_secret": user.totp_secret }, "2fa_verified": True } diff --git a/src/routes/user.py b/src/routes/user.py index 86a31fb..c8c6089 100644 --- a/src/routes/user.py +++ b/src/routes/user.py @@ -12,7 +12,7 @@ class ProfileUpdate(BaseModel): name: Optional[str] = None bio: Optional[str] = None avatar_url: Optional[str] = None - role: Optional[str] = None # V-018: Mass Assignment — allows role escalation + role: Optional[str] = None @router.get("/profile") def get_profile(current_user: User = Depends(get_current_user)): @@ -36,12 +36,7 @@ def update_profile( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): - """ - Update profile. - V-015: Mass Assignment vulnerability if we blindly update attributes. - We are carefully selecting fields here, but V-015 could be planted later - by allowing 'role' update. - """ + """Update profile.""" if data.name is not None: current_user.name = data.name if data.bio is not None: @@ -49,13 +44,12 @@ def update_profile( if data.avatar_url is not None: current_user.avatar_url = data.avatar_url if data.role is not None: - current_user.role = data.role # V-018: Mass Assignment — no authorization check + current_user.role = data.role db.commit() db.refresh(current_user) return {"message": "Profile updated in the swarm database.", "user": current_user} -# User Preferences (V-026 Deserialization) import pickle import base64 @@ -65,16 +59,12 @@ class PreferencesUpdate(BaseModel): @router.get("/preferences") def get_preferences(request: Request): - """ - Get user preferences from cookie. - V-026: Insecure Deserialization via pickle. - """ + """Get user preferences from cookie.""" cookie_value = request.cookies.get("user_prefs") if not cookie_value: return {"theme": "light", "notifications": True} try: - # V-026: Unsafe deserialization decoded = base64.b64decode(cookie_value) prefs = pickle.loads(decoded) return prefs @@ -90,11 +80,10 @@ def set_preferences(data: PreferencesUpdate, response: Response): # Serialize with pickle serialized = base64.b64encode(pickle.dumps(prefs)).decode() - # V-026 constraint: Store as cookie response.set_cookie( key="user_prefs", value=serialized, - httponly=False, # V-008 + httponly=False, secure=False, samesite="Lax" )