You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by as...@apache.org on 2026/04/22 10:43:28 UTC

(tooling-agents) branch main updated: ASVS L3 audit: 3.5.4 (ch03_general)

This is an automated email from the ASF dual-hosted git repository.

asf-tooling pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-agents.git


The following commit(s) were added to refs/heads/main by this push:
     new cc78173  ASVS L3 audit: 3.5.4 (ch03_general)
cc78173 is described below

commit cc78173b4cb78f9f67f73be18286c3cdf99c5c5c
Author: ASF Tooling Team <ro...@apache.org>
AuthorDate: Wed Apr 22 12:43:23 2026 +0200

    ASVS L3 audit: 3.5.4 (ch03_general)
---
 .../reports/steve/v3/d0aa7e9/ch03_general/3.5.4.md | 405 +++++++++++++++++++++
 1 file changed, 405 insertions(+)

diff --git a/repos/apache/ASVS/reports/steve/v3/d0aa7e9/ch03_general/3.5.4.md b/repos/apache/ASVS/reports/steve/v3/d0aa7e9/ch03_general/3.5.4.md
new file mode 100644
index 0000000..cec183c
--- /dev/null
+++ b/repos/apache/ASVS/reports/steve/v3/d0aa7e9/ch03_general/3.5.4.md
@@ -0,0 +1,405 @@
+# ASVS 3.5.4 Security Audit Report: Browser Origin Separation
+
+## Executive Summary
+
+- **Repository:** apache/steve
+- **Audit Date:** Apr 22, 2026
+- **Auditor:** Tooling Agents
+- **ASVS Requirement:** 3.5.4 — Verify that separate applications are hosted on different hostnames to leverage the restrictions provided by same-origin policy, including how documents or scripts loaded by one origin can interact with resources from another origin and hostname-based restrictions on cookies.
+  - **Level:** 2
+  - **Section:** Browser Origin Separation
+  - **Section Description:** When accepting a request to sensitive functionality on the server side, the application needs to ensure the request is initiated by the application itself or by a trusted party and has not been forged by an attacker.
+- **Files Analyzed:** 8 relevant out of 49 total, 12 skipped
+
+**Finding Counts:**
+
+| Severity | Count |
+|----------|-------|
+| Critical | 0 |
+| High | 1 |
+| Medium | 1 |
+| Low | 0 |
+| **Total** | **2** |
+
+**Summary:** This audit evaluates the Apache STeVe v3 voting system against ASVS 3.5.4 requirements for browser origin separation, focusing on whether the application leverages same-origin policy, SameSite cookie logic, and CORS preflight mechanisms to prevent cross-origin request forgery. The application is a single-origin application (positive), but critical gaps exist in how state-changing operations are exposed, undermining the browser security policies that ASVS 3.5.4 relies upon fo [...]
+
+---
+
+## Security Controls Inventory
+
+| Control | Defined | Applied | Coverage |
+|---------|---------|---------|----------|
+| Authentication (`@asfquart.auth.require`) | `pages.py` decorators | All sensitive endpoints | ✅ Complete |
+| CSRF Token Generation | `basic_info()` line ~93 | Generated for all pages | ❌ Placeholder only |
+| CSRF Token Validation | Not implemented | N/A | ❌ **Missing entirely** |
+| Origin/Referer Header Validation | Not implemented | N/A | ❌ Missing |
+| CORS Configuration | Not configured | N/A | ✅ Default (no `Access-Control-Allow-Origin` = restrictive) |
+| SameSite Cookie Attribute | Not visible in codebase | Unknown | ⚠️ Relies on browser defaults |
+| JSON Content-Type (triggers preflight) | `_set_election_date()` | `/do-set-open_at`, `/do-set-close_at` | ✅ Applied to date endpoints |
+| POST method for state changes | Various `/do-*` endpoints | Partial | ❌ **Gap: open/close use GET** |
+
+---
+
+## Critical File Review
+
+### `v3/server/pages.py` — Endpoint Security Matrix
+
+| Endpoint | Method | Auth | CSRF | Origin | SOP Bypass Risk |
+|----------|--------|------|------|--------|-----------------|
+| `GET /` | GET | None | N/A | N/A | None (no state change) |
+| `GET /voter` | GET | Committer | N/A | N/A | None (read-only) |
+| `GET /vote-on/<eid>` | GET | Committer | N/A | N/A | None (read-only) |
+| `GET /admin` | GET | Committer | N/A | N/A | None (read-only) |
+| `GET /manage/<eid>` | GET | Committer | N/A | N/A | None (read-only) |
+| **`GET /do-open/<eid>`** | **GET** | Committer | **❌ None** | **❌ None** | **HIGH** |
+| **`GET /do-close/<eid>`** | **GET** | Committer | **❌ None** | **❌ None** | **HIGH** |
+| `POST /do-set-open_at/<eid>` | POST | Committer | ❌ Placeholder | ❌ None | Low (JSON triggers preflight) |
+| `POST /do-set-close_at/<eid>` | POST | Committer | ❌ Placeholder | ❌ None | Low (JSON triggers preflight) |
+| **`POST /do-vote/<eid>`** | POST | Committer | **❌ Placeholder** | **❌ None** | **MEDIUM** |
+| **`POST /do-create-election`** | POST | PMC | **❌ Placeholder** | **❌ None** | **MEDIUM** |
+| **`POST /do-add-issue/<eid>`** | POST | Committer | **❌ Placeholder** | **❌ None** | **MEDIUM** |
+| **`POST /do-edit-issue/<eid>/<iid>`** | POST | Committer | **❌ Placeholder** | **❌ None** | **MEDIUM** |
+| **`POST /do-delete-issue/<eid>/<iid>`** | POST | Committer | **❌ Placeholder** | **❌ None** | **MEDIUM** |
+| `GET /docs/<iid>/<docname>` | GET | Session | N/A | N/A | None (read-only) |
+
+---
+
+## Findings
+
+### High Severity
+
+#### ASVS-354-HIGH-001: State-Changing Operations via GET Bypass CORS Preflight Protection
+
+**Severity:** HIGH
+
+**Location:**
+- **File:** `v3/server/pages.py`
+- **Functions:** `do_open_endpoint()` (line ~479), `do_close_endpoint()` (line ~498)
+- **Lines:** ~479, ~498
+
+**Related Functions Checked:**
+- `do_open_endpoint()` — Opens election (permanent state change)
+- `do_close_endpoint()` — Closes election (permanent state change)
+- Template: `manage.ezt` (line ~265) — Client-side invocation via GET navigation
+
+**Description:**
+
+The application exposes critical election state-changing operations (`open` and `close`) via GET requests without any CSRF protection, Origin validation, or CORS preflight requirements. This undermines the browser security policies that ASVS 3.5.4 relies upon for request forgery prevention.
+
+**Vulnerable Code:**
+
+```python
+@APP.get('/do-open/<eid>')                          # <-- GET method
+@asfquart.auth.require({R.committer})
+@load_election
+async def do_open_endpoint(election):
+    result = await basic_info()
+    pdb = steve.persondb.PersonDB.open(DB_FNAME)
+    election.open(pdb)                                # <-- Permanent state change
+    ...
+    return quart.redirect(f'/manage/{election.eid}', code=303)
+
+@APP.get('/do-close/<eid>')                          # <-- GET method
+@asfquart.auth.require({R.committer})
+@load_election
+async def do_close_endpoint(election):
+    result = await basic_info()
+    election.close()                                  # <-- Permanent state change
+    ...
+    return quart.redirect(f'/manage/{election.eid}', code=303)
+```
+
+**Triggered from template** (`manage.ezt`, line ~265):
+```javascript
+confirmOpen.addEventListener('click', () => {
+    openModal.hide();
+    window.location.href = '/do-open/[eid]';       // GET navigation
+});
+```
+
+**Data Flow:**
+1. **Source:** Cross-origin attacker page
+2. **Mechanism:** Top-level navigation (link click, `window.location`, `<meta>` redirect)
+3. **Missing Control:** CORS preflight (never applies to GET), no CSRF validation, no Origin header check
+4. **Sink:** `election.open(pdb)` or `election.close()` — permanent state changes
+
+**Why this undermines ASVS 3.5.4:**
+- The CORS preflight mechanism — identified in the ASVS section description as a "useful request forgery prevention mechanism" — **never applies to GET requests**
+- `SameSite=Lax` (browser default) **allows cookies on top-level GET navigations**, so session cookies are sent
+- Both operations are explicitly documented as "permanent" and "cannot be undone" in the UI
+
+**Proof of Concept:**
+
+```html
+<!-- Attacker page hosted on evil.example.com -->
+<html>
+<body>
+  <h1>Click for prize!</h1>
+  <!-- Method 1: User-triggered navigation -->
+  <a href="https://steve.apache.org/do-open/TARGET_EID">Claim Prize</a>
+  
+  <!-- Method 2: Automatic redirect -->
+  <script>window.location = 'https://steve.apache.org/do-close/TARGET_EID';</script>
+  
+  <!-- Method 3: Meta redirect -->
+  <meta http-equiv="refresh" content="0;url=https://steve.apache.org/do-open/TARGET_EID">
+</body>
+</html>
+```
+
+**Impact:**
+- An attacker knowing or guessing an election ID can force an authenticated admin to **permanently open or close an election** by luring them to a malicious page
+- Opening an election prematurely could compromise vote integrity (salts generated before issues are finalized)
+- Closing an election prematurely disenfranchises voters
+- Violates HTTP semantics (GET should be safe and idempotent)
+
+**Remediation:**
+
+```python
+# Change to POST and add CSRF validation
+@APP.post('/do-open/<eid>')
+@asfquart.auth.require({R.committer})
+@load_election
+async def do_open_endpoint(election):
+    result = await basic_info()
+    
+    # Validate CSRF token
+    form = await quart.request.form
+    if form.get('csrf_token') != (await asfquart.session.read()).get('csrf_token'):
+        quart.abort(403, 'CSRF validation failed')
+    
+    # Validate Origin header as defense-in-depth
+    origin = quart.request.headers.get('Origin')
+    if origin and origin != quart.request.host_url.rstrip('/'):
+        quart.abort(403, 'Origin mismatch')
+    
+    pdb = steve.persondb.PersonDB.open(DB_FNAME)
+    election.open(pdb)
+    ...
+```
+
+Update the template to use form submission:
+```javascript
+confirmOpen.addEventListener('click', () => {
+    openModal.hide();
+    const form = document.createElement('form');
+    form.method = 'POST';
+    form.action = '/do-open/[eid]';
+    const csrf = document.createElement('input');
+    csrf.type = 'hidden';
+    csrf.name = 'csrf_token';
+    csrf.value = csrfToken;
+    form.appendChild(csrf);
+    document.body.appendChild(form);
+    form.submit();
+});
+```
+
+---
+
+### Medium Severity
+
+#### ASVS-354-MEDIUM-001: Form-Based POST Endpoints Accept Simple Requests Without Origin Validation
+
+**Severity:** MEDIUM
+
+**Location:**
+- **File:** `v3/server/pages.py`
+- **Functions:** `do_vote_endpoint()` (line ~410), `do_create_endpoint()` (line ~457), `do_add_issue_endpoint()` (line ~515), `do_edit_issue_endpoint()` (line ~538), `do_delete_issue_endpoint()` (line ~559)
+- **Lines:** ~410, ~457, ~515, ~538, ~559
+
+**Related Functions Checked:**
+- `do_vote_endpoint()` — Submit votes
+- `do_create_endpoint()` — Create election
+- `do_add_issue_endpoint()` — Add issue
+- `do_edit_issue_endpoint()` — Edit issue
+- `do_delete_issue_endpoint()` — Delete issue
+- `do_set_open_at_endpoint()` — Set open date
+- `do_set_close_at_endpoint()` — Set close date
+- `basic_info()` (line ~93) — CSRF token generation
+
+**Description:**
+
+All POST endpoints accept `application/x-www-form-urlencoded` content without CSRF token validation or Origin header checking. While CSRF tokens are generated and included in forms, they are never validated server-side (Type B gap: control EXISTS but NOT CALLED). This creates a placeholder security control that provides false confidence.
+
+**Vulnerable Code (representative example):**
+
+```python
+@APP.post('/do-vote/<eid>')
+@asfquart.auth.require({R.committer})
+@load_election
+async def do_vote_endpoint(election):
+    result = await basic_info()
+    ### check authz                          # <-- No authorization beyond authentication
+    form = edict(await quart.request.form)   # <-- Accepts application/x-www-form-urlencoded
+    # No CSRF token validation
+    # No Origin/Referer header check
+    ...
+    election.add_vote(result.uid, iid, votestring)  # <-- Sensitive operation
+```
+
+**CSRF token present in template but never validated server-side:**
+```python
+# pages.py line ~93
+basic.csrf_token = 'placeholder'  # <-- Static, predictable value
+
+# manage.ezt template includes it in forms:
+# <input type="hidden" name="csrf_token" value="[csrf_token]">
+# But no endpoint reads or validates this field
+```
+
+**Gap Type Classification:** Type B (Control EXISTS — CSRF token field in forms — but NOT CALLED — no server-side validation)
+
+**All affected POST endpoints:**
+
+| Endpoint | Operation | CSRF Validated | Origin Checked |
+|----------|-----------|----------------|----------------|
+| `POST /do-vote/<eid>` | Submit votes | ❌ | ❌ |
+| `POST /do-create-election` | Create election | ❌ | ❌ |
+| `POST /do-add-issue/<eid>` | Add issue | ❌ | ❌ |
+| `POST /do-edit-issue/<eid>/<iid>` | Edit issue | ❌ | ❌ |
+| `POST /do-delete-issue/<eid>/<iid>` | Delete issue | ❌ | ❌ |
+| `POST /do-set-open_at/<eid>` | Set open date | ❌ | ❌ |
+| `POST /do-set-close_at/<eid>` | Set close date | ❌ | ❌ |
+
+**Data Flow:**
+1. **Source:** Cross-origin attacker page
+2. **Mechanism:** Auto-submitting HTML form with `application/x-www-form-urlencoded`
+3. **Missing Control:** No CORS preflight (simple content type), no CSRF validation, no Origin check
+4. **Mitigating Factor:** `SameSite=Lax` browser default (if applied) blocks cookies on cross-origin POST
+5. **Sink:** State-changing operations (vote submission, election/issue management)
+
+**Why this matters for ASVS 3.5.4:**
+- `application/x-www-form-urlencoded` is a "simple" content type — **no CORS preflight is triggered**
+- The browser sends the cross-origin POST request with form data without asking the server first
+- The only remaining browser-level protection is SameSite cookie behavior, which is **not explicitly configured** in this codebase
+- The CSRF token field exists in forms (creating false confidence) but is never validated — **Type B gap**
+
+**Mitigating Factor:** `SameSite=Lax` (current browser default) blocks cookies on cross-origin form POST submissions. However, this relies on the browser default and on `asfquart.session` not setting `SameSite=None`.
+
+**Proof of Concept (effective if SameSite is not Lax/Strict):**
+
+```html
+<!-- Auto-submitting cross-origin form -->
+<form id="voteForm" action="https://steve.apache.org/do-vote/TARGET_EID" method="POST">
+  <input name="vote-ISSUE_ID" value="y" />
+  <input name="csrf_token" value="placeholder" />  <!-- Known static value! -->
+</form>
+<script>document.getElementById('voteForm').submit();</script>
+```
+
+**Impact:**
+- If session cookies lack `SameSite=Lax` or `SameSite=Strict`, all state-changing operations are vulnerable to cross-origin form submission
+- Vote manipulation, unauthorized election creation, issue modification/deletion
+- The static `'placeholder'` CSRF token provides zero additional protection since it's predictable
+- Reduced to MEDIUM severity due to reliance on browser default `SameSite=Lax` protection
+
+**Remediation:**
+
+```python
+# 1. Generate real CSRF tokens per session
+async def basic_info():
+    ...
+    s = await asfquart.session.read()
+    if s:
+        if 'csrf_token' not in s:
+            s['csrf_token'] = crypto.gen_salt()  # Reuse salt generation
+            await asfquart.session.write(s)
+        basic.csrf_token = s['csrf_token']
+    ...
+
+# 2. Create validation decorator
+def require_csrf(func):
+    @functools.wraps(func)
+    async def wrapper(*args, **kwargs):
+        s = await asfquart.session.read()
+        session_token = s.get('csrf_token') if s else None
+        
+        # Check form data or JSON body or header
+        form = await quart.request.form
+        request_token = (form.get('csrf_token') 
+                        or quart.request.headers.get('X-CSRFToken'))
+        
+        if not session_token or not request_token or session_token != request_token:
+            quart.abort(403, 'CSRF validation failed')
+        return await func(*args, **kwargs)
+    return wrapper
+
+# 3. Apply to all POST endpoints
+@APP.post('/do-vote/<eid>')
+@asfquart.auth.require({R.committer})
+@require_csrf
+@load_election
+async def do_vote_endpoint(election):
+    ...
+
+# 4. Explicitly set SameSite on session cookies
+app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
+app.config['SESSION_COOKIE_SECURE'] = True
+```
+
+---
+
+## Positive Security Patterns
+
+1. **JSON Content-Type for date-setting endpoints:** The `/do-set-open_at/<eid>` and `/do-set-close_at/<eid>` endpoints accept `application/json`, which triggers CORS preflight for cross-origin requests. Since no `Access-Control-Allow-Origin` header is set, cross-origin JavaScript-initiated requests are blocked at preflight.
+
+2. **No permissive CORS headers:** The application does not set `Access-Control-Allow-Origin` or related CORS headers anywhere, meaning the default restrictive browser policy applies. Cross-origin JavaScript cannot read responses from any endpoint.
+
+3. **Single-origin application architecture:** All application components (pages, API, static assets) are served from the same hostname, which is the correct pattern for a monolithic application and avoids the need for cross-origin allowances.
+
+4. **Authentication required on all sensitive endpoints:** Every state-changing and data-reading endpoint uses `@asfquart.auth.require()` with appropriate role requirements, providing a baseline authentication layer.
+
+5. **EZT template output escaping:** The `manage.ezt` template uses `[format "js,html"]` for escaping values injected into JavaScript strings, preventing reflected XSS that could bypass same-origin protections.
+
+---
+
+## Architecture Observations
+
+1. **Single hostname deployment is correct for this application:** ASVS 3.5.4 requires separation for *separate* applications. This is a single application, and serving all components from one origin is appropriate.
+
+2. **Defense-in-depth gap in request forgery prevention:** The application relies on a single layer (browser cookie SameSite defaults) for cross-origin protection. The ASVS section description identifies three complementary mechanisms: SameSite cookies, CORS preflight, and Origin validation. Only SameSite (implicitly, via browser defaults) is partially active.
+
+3. **GET-based state changes are an architectural anti-pattern:** Using GET for operations like opening/closing elections violates both HTTP semantics (GET should be safe/idempotent) and undermines every browser-based cross-origin protection mechanism. This pattern likely originated from convenience (simple link-based navigation) but creates a fundamental security weakness.
+
+---
+
+## Recommendations Summary
+
+### Immediate Actions
+
+| Priority | Action | Addresses |
+|----------|--------|-----------|
+| **1** | Convert `/do-open/<eid>` and `/do-close/<eid>` from GET to POST with form submission | ASVS-354-HIGH-001 |
+| **2** | Implement real per-session CSRF tokens with server-side validation on all POST endpoints | ASVS-354-HIGH-001, ASVS-354-MEDIUM-001 |
+
+### Short-term Actions
+
+| Priority | Action | Addresses |
+|----------|--------|-----------|
+| **3** | Add `SameSite=Lax; Secure` attributes explicitly to session cookies | ASVS-354-MEDIUM-001 (defense-in-depth) |
+| **4** | Add Origin header validation as secondary check on state-changing endpoints | ASVS-354-HIGH-001, ASVS-354-MEDIUM-001 (defense-in-depth) |
+
+### Long-term Actions
+
+| Priority | Action | Addresses |
+|----------|--------|-----------|
+| **5** | Consider migrating remaining form POST endpoints to JSON content types where feasible (e.g., vote submission via fetch API) to gain CORS preflight protection | ASVS-354-MEDIUM-001 (defense-in-depth) |
+
+---
+
+## Appendix: Files Analyzed
+
+<details>
+<summary>Click to expand file list (8 files)</summary>
+
+1. `v3/server/pages.py` (relevance: 8)
+2. `v3/server/main.py` (relevance: 4)
+3. `v3/ARCHITECTURE.md` (relevance: 3)
+4. `v3/TODO.md` (relevance: 2)
+5. `v3/server/config.yaml.example` (relevance: 2)
+6. `v3/server/static/js/steve.js` (relevance: 2)
+7. `v3/server/templates/manage.ezt` (relevance: 2)
+8. `v3/steve/election.py` (relevance: 2)
+
+</details>
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org