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:28:07 UTC

(tooling-agents) branch main updated: ASVS L3 audit: 3.5.1 (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 beba4db  ASVS L3 audit: 3.5.1 (ch03_general)
beba4db is described below

commit beba4db7d94cea77b86de84eaca0940e319e297e
Author: ASF Tooling Team <ro...@apache.org>
AuthorDate: Wed Apr 22 12:28:03 2026 +0200

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

diff --git a/repos/apache/ASVS/reports/steve/v3/d0aa7e9/ch03_general/3.5.1.md b/repos/apache/ASVS/reports/steve/v3/d0aa7e9/ch03_general/3.5.1.md
new file mode 100644
index 0000000..4aa9f1c
--- /dev/null
+++ b/repos/apache/ASVS/reports/steve/v3/d0aa7e9/ch03_general/3.5.1.md
@@ -0,0 +1,542 @@
+# ASVS 3.5.1 Security Audit Report
+
+## Executive Summary
+
+**Repository:** apache/steve  
+**Audit Date:** Apr 22, 2026  
+**Auditor:** Tooling Agents  
+**ASVS Requirement:** 3.5.1 — Verify that, if the application does not rely on the CORS preflight mechanism to prevent disallowed cross-origin requests to use sensitive functionality, these requests are validated to ensure they originate from the application itself. This may be done by using and validating anti-forgery tokens or requiring extra HTTP header fields that are not CORS-safelisted request-header fields. This is to defend against browser-based request forgery attacks, commonly  [...]
+
+**Files Analyzed:** 6 relevant out of 49 total, 12 skipped
+
+**Finding Summary:**
+
+| Severity | Count |
+|----------|-------|
+| Critical | 1 |
+| High | 2 |
+| Medium | 0 |
+| Low | 0 |
+| **Total** | **3** |
+
+**Overall Assessment:** The application has **no functional CSRF protection**. A CSRF token mechanism is partially scaffolded in templates and JavaScript, but the server-side token is a hardcoded string `'placeholder'` that is never validated on any endpoint. Additionally, two highly sensitive state-changing operations (opening and closing elections) are implemented as **GET requests**, making them trivially exploitable through image tags or link prefetch, bypassing even SameSite=Lax coo [...]
+
+---
+
+## Security Controls Inventory
+
+| Control | Defined At | Status | Coverage |
+|---------|-----------|--------|----------|
+| CSRF token generation | `basic_info()` line ~95 | **PLACEHOLDER** — hardcoded `'placeholder'` string | All templates receive it |
+| CSRF token in HTML forms | `manage.ezt`, `admin.ezt`, `vote-on.ezt` | Present in `<input type="hidden" name="csrf_token">` | 5 forms |
+| CSRF token in JS (X-CSRFToken header) | `manage.ezt` line ~290 | Sent via `fetch()` headers | 2 JSON endpoints |
+| Server-side CSRF validation | — | **DOES NOT EXIST** | 0 endpoints |
+| Authentication decorator | `@asfquart.auth.require` | Functional | All state-changing endpoints |
+| CORS preflight (implicit) | JSON endpoints (`Content-Type: application/json`) | Implicit — triggers preflight | 2 endpoints only |
+
+### Coverage Gap Summary
+
+| Endpoint | HTTP Method | Content-Type | CSRF Token Sent? | CSRF Validated? | CORS Preflight? | Gap Type |
+|----------|------------|-------------|-------------------|-----------------|-----------------|----------|
+| `/do-open/<eid>` | **GET** | N/A | No | No | **No** | Type A |
+| `/do-close/<eid>` | **GET** | N/A | No | No | **No** | Type A |
+| `/do-vote/<eid>` | POST | form-urlencoded | Yes (placeholder) | **No** | **No** | Type B |
+| `/do-create-election` | POST | form-urlencoded | Yes (placeholder) | **No** | **No** | Type B |
+| `/do-add-issue/<eid>` | POST | form-urlencoded | Yes (placeholder) | **No** | **No** | Type B |
+| `/do-edit-issue/<eid>/<iid>` | POST | form-urlencoded | Yes (placeholder) | **No** | **No** | Type B |
+| `/do-delete-issue/<eid>/<iid>` | POST | form-urlencoded | Yes (placeholder) | **No** | **No** | Type B |
+| `/do-set-open_at/<eid>` | POST | application/json | Yes (X-CSRFToken) | **No** | Yes (implicit) | Type B |
+| `/do-set-close_at/<eid>` | POST | application/json | Yes (X-CSRFToken) | **No** | Yes (implicit) | Type B |
+
+---
+
+## Critical File Review
+
+### State-Changing Endpoints Analysis
+
+| File | State-Changing Routes | CSRF Token Present | CSRF Validated | Verdict |
+|------|----------------------|-------------------|----------------|---------|
+| `pages.py` | 9 endpoints | Token generated as `'placeholder'` | **Never** | ❌ FAIL |
+| `manage.ezt` | Forms for add/edit/delete issue; JS for open/close/dates | Token in hidden inputs + JS header | N/A (client-side) | ⚠️ Client sends token, server ignores |
+| `vote-on.ezt` | Vote submission form | Token in hidden input | N/A (client-side) | ⚠️ Client sends token, server ignores |
+| `admin.ezt` | Create election form | Token in hidden input | N/A (client-side) | ⚠️ Client sends token, server ignores |
+| `manage-stv.ezt` | Forms for add/edit/delete issue; JS for open/close | Token in hidden inputs | N/A (client-side) | ⚠️ Client sends token, server ignores |
+
+---
+
+## Findings
+
+### Critical Severity
+
+#### ASVS-351-CRT-001: CSRF Token Is a Hardcoded Placeholder; Server Never Validates It
+
+**Severity:** CRITICAL  
+**Gap Type:** Type B — Control EXISTS but NOT CALLED (false confidence)  
+**File:** `v3/server/pages.py`  
+**Function:** `basic_info()`  
+**Line:** ~95
+
+**Related Functions Checked:**
+- `do_vote_endpoint()` (line ~438)
+- `do_create_endpoint()` (line ~478)
+- `do_add_issue_endpoint()`
+- `do_edit_issue_endpoint()`
+- `do_delete_issue_endpoint()`
+- `_set_election_date()`
+
+**Description:**
+
+The application generates a hardcoded placeholder CSRF token that is never validated server-side, creating a false sense of security. While templates correctly include CSRF tokens in forms and JavaScript correctly sends them in headers, all POST endpoints completely ignore these tokens during request processing.
+
+**Vulnerable Code — Token generation:**
+
+```python
+# v3/server/pages.py, line ~95
+async def basic_info():
+    ...
+    ### generate a real token and store in the session
+    basic.csrf_token = 'placeholder'
+    return basic
+```
+
+**Templates that include the token (giving appearance of protection):**
+
+`manage.ezt`:
+```html
+<input type="hidden" id="csrf-token" value="[csrf_token]">
+...
+<form id="issueForm" method="POST" action="">
+    <input type="hidden" name="csrf_token" value="[csrf_token]">
+```
+
+`vote-on.ezt`:
+```html
+<form id="voteForm" method="POST" action="/do-vote/[eid]">
+    <input type="hidden" name="csrf_token" value="[csrf_token]">
+```
+
+`admin.ezt`:
+```html
+<form id="createElectionForm" method="POST" action="/do-create-election">
+    <input type="hidden" name="csrf_token" value="[csrf_token]">
+```
+
+**Server-side validation — ABSENT from all POST handlers:**
+
+`do_vote_endpoint` (line ~438):
+```python
+async def do_vote_endpoint(election):
+    result = await basic_info()
+    form = edict(await quart.request.form)
+    # csrf_token is in form data but NEVER checked
+    if not form:
+        await flash_danger('No vote data provided.')
+        ...
+    votes = {}
+    for key, value in form.items():
+        if key.startswith('vote-'):
+            ...
+```
+
+`do_create_endpoint` (line ~478):
+```python
+async def do_create_endpoint():
+    result = await basic_info()
+    form = edict(await quart.request.form)
+    # csrf_token is in form data but NEVER checked
+    election = steve.election.Election.create(DB_FNAME, form.title, result.uid)
+```
+
+**Data Flow:**
+
+```
+basic_info() 
+  → generates static 'placeholder' token 
+  → template embeds it in forms 
+  → POST endpoints read form data 
+  → csrf_token field is ignored 
+  → state change executes without validation
+```
+
+**Proof of Concept:**
+
+Any cross-origin page can submit a form to any POST endpoint. The `csrf_token` field value is irrelevant — it can be anything or omitted entirely:
+
+```html
+<!-- Cast a vote on behalf of the victim -->
+<form id="csrf" method="POST" action="https://steve.example.org/do-vote/a1b2c3d4">
+  <input type="hidden" name="vote-issue1" value="y">
+  <input type="hidden" name="vote-issue2" value="n">
+  <!-- csrf_token can be anything; server doesn't check -->
+  <input type="hidden" name="csrf_token" value="anything">
+</form>
+<script>document.getElementById('csrf').submit();</script>
+```
+
+**Impact:**
+
+- **All POST endpoints are fully vulnerable to CSRF attacks** despite templates containing CSRF token fields
+- Creates **false confidence** — code reviewers and template auditors may assume CSRF is handled
+- The `TODO.md` file (item #6 and #8) explicitly acknowledges this: *"CSRF Token Placeholder ... CSRF protection is bypassed"*
+- Affected endpoints and operations:
+  - **Vote submission** (`/do-vote/<eid>`) — attacker can cast or change votes
+  - **Election creation** (`/do-create-election`) — attacker can create elections
+  - **Issue management** (`/do-add-issue`, `/do-edit-issue`, `/do-delete-issue`) — attacker can tamper with ballot content
+  - **Date setting** (`/do-set-open_at`, `/do-set-close_at`) — mitigated by CORS preflight (JSON Content-Type)
+
+**Remediation:**
+
+Implement real CSRF token generation and validation:
+
+```python
+import secrets
+
+async def basic_info():
+    ...
+    s = await asfquart.session.read()
+    if s:
+        # Generate or retrieve CSRF token from session
+        if 'csrf_token' not in s:
+            s['csrf_token'] = secrets.token_hex(32)
+            await asfquart.session.write(s)
+        basic.csrf_token = s['csrf_token']
+    else:
+        basic.csrf_token = None
+    return basic
+
+
+async def validate_csrf_token():
+    """Validate CSRF token from form data or X-CSRFToken header."""
+    s = await asfquart.session.read()
+    if not s or 'csrf_token' not in s:
+        quart.abort(403, 'Missing session CSRF token')
+    
+    expected = s['csrf_token']
+    
+    # Check form data first, then header
+    if quart.request.content_type and 'json' in quart.request.content_type:
+        actual = quart.request.headers.get('X-CSRFToken')
+    else:
+        form = await quart.request.form
+        actual = form.get('csrf_token')
+    
+    if not actual or not secrets.compare_digest(actual, expected):
+        quart.abort(403, 'Invalid CSRF token')
+```
+
+Apply to all state-changing endpoints:
+```python
+@APP.post('/do-vote/<eid>')
+@asfquart.auth.require({R.committer})
+@load_election
+async def do_vote_endpoint(election):
+    await validate_csrf_token()  # Add this line
+    result = await basic_info()
+    ...
+```
+
+---
+
+### High Severity
+
+#### ASVS-351-HGH-001: State-Changing Operations on GET Requests Enable Trivial CSRF
+
+**Severity:** HIGH  
+**Gap Type:** Type A — Entry point with NO control  
+**File:** `v3/server/pages.py`  
+**Functions:** `do_open_endpoint()`, `do_close_endpoint()`  
+**Lines:** ~504, ~523
+
+**Related Functions Checked:**
+- JavaScript event handlers in `manage.ezt` (lines ~285, ~297)
+- Modal confirmation logic
+
+**Description:**
+
+Two highly sensitive state-changing operations (opening and closing elections) are implemented as GET requests, making them trivially exploitable through image tags, link prefetch, or simple hyperlinks. This bypasses even SameSite=Lax cookie protections and violates REST semantics.
+
+**Vulnerable Code — Route definitions (lines ~504, ~523):**
+
+```python
+@APP.get('/do-open/<eid>')
+@asfquart.auth.require({R.committer})
+@load_election
+async def do_open_endpoint(election):
+    ...
+    election.open(pdb)
+    ...
+    return quart.redirect(f'/manage/{election.eid}', code=303)
+
+
+@APP.get('/do-close/<eid>')
+@asfquart.auth.require({R.committer})
+@load_election
+async def do_close_endpoint(election):
+    ...
+    election.close()
+    ...
+    return quart.redirect(f'/manage/{election.eid}', code=303)
+```
+
+**Invoked from JavaScript (manage.ezt lines ~285, ~297):**
+
+```javascript
+confirmOpen.addEventListener('click', () => {
+    openModal.hide();
+    window.location.href = '/do-open/[eid]';
+});
+...
+confirmClose.addEventListener('click', () => {
+    closeModal.hide();
+    window.location.href = '/do-close/[eid]';
+});
+```
+
+**Data Flow:**
+
+```
+Attacker-crafted page (img/link/iframe) 
+  → GET /do-open/<eid> or GET /do-close/<eid> 
+  → election.open() / election.close() 
+  → Irreversible election state change
+```
+
+No CSRF token, no custom header, no preflight check possible.
+
+**Proof of Concept:**
+
+An attacker sends an email or message to an election administrator containing:
+
+```html
+<!-- Open an election without the admin's knowledge -->
+<img src="https://steve.example.org/do-open/a1b2c3d4" width="0" height="0">
+
+<!-- Or close one -->
+<img src="https://steve.example.org/do-close/a1b2c3d4" width="0" height="0">
+```
+
+When the admin views this content while authenticated:
+- The browser sends a GET request with session cookies (SameSite=Lax allows this for top-level navigation, and `<img>` tags send cookies with SameSite=None or absent; for Lax, an attacker could use a top-level redirect: `<a href="...">Click here</a>`)
+- The election is opened or closed **permanently and irreversibly**
+- No confirmation prompt is triggered (the server-side modal is client-side only)
+
+**Impact:**
+
+- An attacker can **permanently open** an election before it's ready (exposing draft ballots, enabling premature voting)
+- An attacker can **permanently close** an active election (disenfranchising voters who haven't yet voted)
+- Both actions are described in the UI as **irreversible** ("This action is permanent")
+- Only requires the attacker to know or guess the election ID (EID)
+
+**Remediation:**
+
+Change these to POST endpoints with CSRF token validation:
+
+```python
+@APP.post('/do-open/<eid>')
+@asfquart.auth.require({R.committer})
+@load_election
+async def do_open_endpoint(election):
+    await validate_csrf_token()  # Verify token from form/header
+    result = await basic_info()
+    pdb = steve.persondb.PersonDB.open(DB_FNAME)
+    election.open(pdb)
+    _LOGGER.info(f'User[U:{result.uid}] opened election[E:{election.eid}]')
+    title = election.get_metadata().title
+    await flash_success(f'Opened election: {title}')
+    return quart.redirect(f'/manage/{election.eid}', code=303)
+```
+
+Update template JavaScript to use form submission:
+
+```javascript
+confirmOpen.addEventListener('click', () => {
+    openModal.hide();
+    const form = document.createElement('form');
+    form.method = 'POST';
+    form.action = '/do-open/[eid]';
+    const token = document.createElement('input');
+    token.type = 'hidden';
+    token.name = 'csrf_token';
+    token.value = csrfToken;
+    form.appendChild(token);
+    document.body.appendChild(form);
+    form.submit();
+});
+```
+
+---
+
+#### ASVS-351-HGH-002: Vote Submission Endpoint Lacks CSRF Protection, Enabling Vote Manipulation
+
+**Severity:** HIGH  
+**Gap Type:** Type B — Token exists in form but server ignores it  
+**File:** `v3/server/pages.py`  
+**Function:** `do_vote_endpoint()`  
+**Line:** ~438
+
+**Related Functions Checked:**
+- `election.add_vote()` (called from do_vote_endpoint)
+- Form parsing logic in do_vote_endpoint
+- Template `vote-on.ezt` form generation
+
+**Description:**
+
+The vote submission endpoint processes votes without validating the CSRF token, despite the token being present in the form. This allows attackers to cast or modify votes on behalf of authenticated users.
+
+**Vulnerable Code:**
+
+```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
+
+    # Parse the form data
+    form = edict(await quart.request.form)
+    if not form:
+        await flash_danger('No vote data provided.')
+        return quart.redirect(f'/vote-on/{election.eid}', code=303)
+
+    # Extract votes from form (keys like 'vote-<iid>')
+    votes = {}
+    for key, value in form.items():
+        if key.startswith('vote-'):
+            iid = key.split('-', 1)[1]
+            votes[iid] = value
+
+    # Process each vote - NO CSRF CHECK BEFORE THIS
+    for iid, votestring in votes.items():
+        ...
+        election.add_vote(result.uid, iid, votestring)
+```
+
+**Template (vote-on.ezt) includes token but it's meaningless:**
+
+```html
+<form id="voteForm" method="POST" action="/do-vote/[eid]">
+    <input type="hidden" name="csrf_token" value="[csrf_token]">
+```
+
+**Data Flow:**
+
+```
+Attacker-controlled form 
+  → POST /do-vote/<eid> with Content-Type: application/x-www-form-urlencoded 
+  → cookies sent (CORS-safelisted) 
+  → election.add_vote() called 
+  → votes recorded/overwritten without origin verification
+```
+
+**Proof of Concept:**
+
+```html
+<html>
+<body onload="document.getElementById('f').submit()">
+<form id="f" method="POST" action="https://steve.example.org/do-vote/KNOWN-EID">
+  <!-- Vote 'yes' on all issues -->
+  <input name="vote-issue-001" value="y">
+  <input name="vote-issue-002" value="y">
+  <input name="vote-issue-003" value="y">
+</form>
+</body>
+</html>
+```
+
+If the voter visits this page while authenticated, their votes are silently cast or overwritten. The voter is redirected to `/voter` with a "Votes submitted successfully!" flash message, which they may never see.
+
+**Impact:**
+
+- Attacker can **cast votes** on behalf of any authenticated voter
+- Attacker can **overwrite existing votes** (the `add_vote` call appears to upsert)
+- Undermines the integrity of election results
+- Attack is invisible to the voter unless they check their ballot afterward
+
+**Remediation:**
+
+Add CSRF validation as the first operation:
+
+```python
+@APP.post('/do-vote/<eid>')
+@asfquart.auth.require({R.committer})
+@load_election
+async def do_vote_endpoint(election):
+    await validate_csrf_token()  # MUST be first
+    result = await basic_info()
+    ...
+```
+
+---
+
+## Positive Security Patterns
+
+1. **Authentication consistently applied**: All state-changing endpoints use `@asfquart.auth.require` decorators with appropriate role requirements (`R.committer`, `R.pmc_member`)
+
+2. **POST used for most mutations**: Most state-changing endpoints correctly use POST method (except do-open/do-close)
+
+3. **JSON endpoints implicitly trigger CORS preflight**: `/do-set-open_at` and `/do-set-close_at` use `request.get_json()` requiring `Content-Type: application/json`, which is not CORS-safelisted and would trigger preflight checks
+
+4. **SRI on external scripts**: `vote-on.ezt` includes `integrity="sha384-..."` on the SortableJS script tag
+
+5. **Template escaping**: Templates use `[format "js,html"]` for JavaScript contexts
+
+6. **CSRF infrastructure partially scaffolded**: Templates, hidden inputs, and JS headers are in place — only the server-side generation and validation are missing, making this a focused fix
+
+---
+
+## Architecture Observations
+
+1. **CSRF token infrastructure is 80% built**: The frontend (templates + JavaScript) correctly includes CSRF tokens in forms and fetch headers. The only missing pieces are:
+   - Replace `'placeholder'` with `secrets.token_hex(32)` in session
+   - Add a `validate_csrf_token()` function
+   - Call it from each POST handler
+
+2. **GET-based state changes are an architectural flaw**: Using `@APP.get` for `/do-open/<eid>` and `/do-close/<eid>` violates REST semantics and creates a CSRF surface that cannot be mitigated by SameSite=Lax cookies alone. This requires a method change to POST.
+
+3. **The `manage.ezt` and `manage-stv.ezt` templates share the same vulnerability pattern**: Both use `window.location.href` for open/close actions, both have forms with placeholder CSRF tokens.
+
+4. **The `_set_election_date` helper receives JSON**: This means the two date-setting endpoints benefit from implicit CORS preflight protection (assuming no permissive CORS policy), making them lower priority for CSRF remediation compared to the form-encoded endpoints.
+
+---
+
+## Recommendations Summary
+
+### Immediate Actions (P0)
+
+| Priority | Action | Endpoints Affected | Effort |
+|----------|--------|-------------------|--------|
+| **P0** | Change `/do-open/<eid>` and `/do-close/<eid>` from GET to POST | 2 endpoints + 2 templates | Low |
+| **P0** | Implement `validate_csrf_token()` server-side function | All POST endpoints | Low |
+| **P0** | Replace `'placeholder'` with real per-session token via `secrets.token_hex()` | `basic_info()` | Low |
+
+### Short-term Actions (P1)
+
+| Priority | Action | Endpoints Affected | Effort |
+|----------|--------|-------------------|--------|
+| **P1** | Add `await validate_csrf_token()` call to `/do-vote/<eid>` | 1 endpoint | Trivial |
+| **P1** | Add `await validate_csrf_token()` call to all other POST endpoints | 6 endpoints | Low |
+
+### Long-term Actions (P2)
+
+| Priority | Action | Endpoints Affected | Effort |
+|----------|--------|-------------------|--------|
+| **P2** | Verify CORS configuration does not permit cross-origin requests to JSON endpoints | Server config | Low |
+| **P2** | Consider implementing CSRF validation as a decorator or middleware to ensure automatic coverage of future endpoints | Architecture | Medium |
+
+---
+
+## Appendix: Files Analyzed
+
+<details>
+<summary>Click to expand file list (6 files)</summary>
+
+- `v3/server/pages.py` (relevance: 10)
+- `v3/TODO.md` (relevance: 9)
+- `v3/server/templates/manage.ezt` (relevance: 6)
+- `v3/server/templates/manage-stv.ezt` (relevance: 5)
+- `v3/server/templates/vote-on.ezt` (relevance: 5)
+- `v3/server/templates/admin.ezt` (relevance: 4)
+
+</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