Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="<script>alert('XSS')</script> Boring.", is_approved=True), # V-003 XSS payload
Review(product_id=p1.id, user_id=hacker.id, rating=1, comment="<script>alert('XSS')</script> 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)
Expand Down
19 changes: 4 additions & 15 deletions src/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,19 @@
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})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
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.
Expand All @@ -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):
Expand All @@ -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(
Expand Down
16 changes: 3 additions & 13 deletions src/cart.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
7 changes: 1 addition & 6 deletions src/email_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 0 additions & 2 deletions src/frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@
<body class="bg-[#0D0A1A]">
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
<!-- V-004: Legacy AngularJS 1.7.7 (deliberately vulnerable) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.7/angular.min.js"></script>
<script src="/legacy/angular_widget.js"></script>
<!-- V-007: Prototype Pollution via deepMerge + V-003: DOM XSS via hash fragment -->
<script src="/legacy/custom.js"></script>
</body>
</html>
17 changes: 2 additions & 15 deletions src/frontend/legacy/angular_widget.js
Original file line number Diff line number Diff line change
@@ -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++;
Expand All @@ -28,29 +21,23 @@
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 =
'<div style="font-size:12px;color:#8866bb;margin-bottom:8px;">Legacy Search (AngularJS 1.7.7)</div>' +
'<div style="font-size:16px;">Results for: ' + legacyQ + '</div>';

// Insert at the top of the page content
var main = root.querySelector('main') || root.firstElementChild;
if (main) {
main.insertBefore(widget, main.firstChild);
} else {
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);
})();
10 changes: 0 additions & 10 deletions src/frontend/legacy/custom.js
Original file line number Diff line number Diff line change
@@ -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];
Expand All @@ -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');
Expand All @@ -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',
Expand All @@ -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) {
Expand All @@ -77,7 +68,6 @@ if (typeof window !== 'undefined') {
}
});

// Expose functions globally for testing
window.BugStore = {
deepMerge: deepMerge,
subscribeNewsletter: subscribeNewsletter
Expand Down
17 changes: 2 additions & 15 deletions src/frontend/public/legacy/angular_widget.js
Original file line number Diff line number Diff line change
@@ -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++;
Expand All @@ -28,29 +21,23 @@
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 =
'<div style="font-size:12px;color:#8866bb;margin-bottom:8px;">Legacy Search (AngularJS 1.7.7)</div>' +
'<div style="font-size:16px;">Results for: ' + legacyQ + '</div>';

// Insert at the top of the page content
var main = root.querySelector('main') || root.firstElementChild;
if (main) {
main.insertBefore(widget, main.firstChild);
} else {
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);
})();
10 changes: 0 additions & 10 deletions src/frontend/public/legacy/custom.js
Original file line number Diff line number Diff line change
@@ -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];
Expand All @@ -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');
Expand All @@ -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',
Expand All @@ -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) {
Expand All @@ -77,7 +68,6 @@ if (typeof window !== 'undefined') {
}
});

// Expose functions globally for testing
window.BugStore = {
deepMerge: deepMerge,
subscribeNewsletter: subscribeNewsletter
Expand Down
Loading