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 17:53:47 UTC

(tooling-agents) branch main updated: ASVS L3 audit: 8.3.2 (ch08_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 dd7d382  ASVS L3 audit: 8.3.2 (ch08_general)
dd7d382 is described below

commit dd7d382dcf9c8e2cc9b0f7545b3345da1de8d57d
Author: ASF Tooling Team <ro...@apache.org>
AuthorDate: Wed Apr 22 19:53:42 2026 +0200

    ASVS L3 audit: 8.3.2 (ch08_general)
---
 .../reports/steve/v3/d0aa7e9/ch08_general/8.3.2.md | 569 +++++++++++++++++++++
 1 file changed, 569 insertions(+)

diff --git a/repos/apache/ASVS/reports/steve/v3/d0aa7e9/ch08_general/8.3.2.md b/repos/apache/ASVS/reports/steve/v3/d0aa7e9/ch08_general/8.3.2.md
new file mode 100644
index 0000000..28d33a9
--- /dev/null
+++ b/repos/apache/ASVS/reports/steve/v3/d0aa7e9/ch08_general/8.3.2.md
@@ -0,0 +1,569 @@
+# ASVS 8.3.2 Security Audit Report
+
+## Executive Summary
+
+- **Repository:** apache/steve
+- **Audit Date:** Apr 22, 2026
+- **Auditor:** Tooling Agents
+- **ASVS Requirement:** 8.3.2 — Verify that changes to values on which authorization decisions are made are applied immediately. Where changes cannot be applied immediately, (such as when relying on data in self-contained tokens), there must be mitigating controls to alert when a consumer performs an action when they are no longer authorized to do so and revert the change. Note that this alternative would not mitigate information leakage.
+  - **Level:** 3
+  - **Section:** Operation Level Authorization
+  - **Section Description:** The immediate application of authorization changes in the appropriate tier of an application's architecture is crucial to preventing unauthorized actions, especially in dynamic environments.
+  - **Chapter:** Authorization
+  - **Control Objective:** Authorization ensures that access is granted only to permitted consumers (users, servers, and other clients). To enforce the Principle of Least Privilege (POLP), verified applications must meet the following high-level requirements:
+    - Document authorization rules, including decision-making factors and environmental contexts.
+    - Consumers should have access only to resources permitted by their defined entitlements.
+
+**Files Analyzed:** 3 relevant out of 49 total, 12 skipped
+
+**Finding Summary:**
+
+| Severity | Count | IDs |
+|----------|-------|-----|
+| Critical | 1 | ASVS-832-SEV-001 |
+| High | 1 | ASVS-832-SEV-002 |
+| Medium | 1 | ASVS-832-SEV-003 |
+| Low | 0 | — |
+| **Total** | **3** | |
+
+---
+
+## Security Controls Inventory
+
+### Authorization Controls Defined
+
+| Control | Location | Purpose | Status |
+|---------|----------|---------|--------|
+| `owner_pid` field | `election` table / `queries.yaml` | Identify election owner for management authorization | **DEFINED but NEVER CHECKED** |
+| `authz` field | `election` table / `queries.yaml` | Store authorization metadata for election access | **DEFINED but NEVER CHECKED** |
+| `assert self.is_editable()` | `election.py` lines 68, 86, 122, 170, 185, 196, 212 | Prevent modifications to non-editable elections | Applied at data layer ✓ |
+| `_all_metadata(self.S_OPEN)` | `election.py` lines 229, 278 | Require open state for voting | Applied at data layer ✓ |
+| `_all_metadata(self.S_CLOSED)` | `election.py` line 248 | Require closed state for tallying | Applied at data layer ✓ |
+| `q_get_mayvote` check | `election.py` line 233 | Verify voter eligibility per issue | Crash-based, not explicit ⚠️ |
+| `q_find_issues` eligibility check | `pages.py` lines 264-269 | Verify voter has any election-level mayvote entries | Applied on page load only ⚠️ |
+| `@asfquart.auth.require({R.committer})` | `pages.py` (multiple endpoints) | Require committer authentication | Applied ✓ |
+| `@asfquart.auth.require({R.pmc_member})` | `pages.py` line 341 | Require PMC member for election creation | Applied ✓ |
+
+### Coverage Analysis — Owner/Authz Authorization
+
+| Endpoint | Auth | State Check | Owner/Authz Check | Gap Type |
+|----------|------|-------------|-------------------|----------|
+| `GET /manage/<eid>` | ✓ | N/A | ✗ **MISSING** | Type B |
+| `GET /do-open/<eid>` | ✓ | ✓ assert | ✗ **MISSING** | Type B |
+| `GET /do-close/<eid>` | ✓ | ✓ assert | ✗ **MISSING** | Type B |
+| `POST /do-add-issue/<eid>` | ✓ | ✓ assert | ✗ **MISSING** | Type B |
+| `POST /do-edit-issue/<eid>/<iid>` | ✓ | ✓ assert | ✗ **MISSING** | Type B |
+| `POST /do-delete-issue/<eid>/<iid>` | ✓ | ✓ assert | ✗ **MISSING** | Type B |
+| `POST /do-set-open_at/<eid>` | ✓ | N/A | ✗ **MISSING** | Type B |
+| `POST /do-set-close_at/<eid>` | ✓ | N/A | ✗ **MISSING** | Type B |
+| `POST /do-vote/<eid>` | ✓ | ✓ add_vote | ✗ **MISSING** | Type B |
+
+---
+
+## Critical File Review
+
+### `v3/server/pages.py`
+
+| Line(s) | Function | Security Relevance | Status |
+|---------|----------|-------------------|--------|
+| ~193-212 | `load_election` | Loads election, should verify authz | ⚠️ `### check authz` TODO |
+| ~216-248 | `load_election_issue` | Loads election+issue, should verify authz | ⚠️ `### check authz` TODO |
+| ~264-269 | `vote_on_page` | Voter eligibility check | ✓ Explicit check |
+| ~308-338 | `do_vote_endpoint` | Vote submission, no eligibility pre-check | ⚠️ Relies on crash |
+| ~341-361 | `do_create_endpoint` | Election creation | ✓ R.pmc_member |
+| ~371-383 | `do_open_endpoint` | Opens election via GET, no owner check | ✗✗ Two issues |
+| ~386-398 | `do_close_endpoint` | Closes election via GET, no owner check | ✗✗ Two issues |
+| ~401-418 | `do_add_issue_endpoint` | Adds issue, no owner check | ✗ Missing authz |
+| ~420-438 | `do_edit_issue_endpoint` | Edits issue, no owner check | ✗ Missing authz |
+| ~440-454 | `do_delete_issue_endpoint` | Deletes issue, no owner check | ✗ Missing authz |
+| ~498-506 | `serve_doc` | Document serving with mayvote check | ✓ Explicit check |
+
+### `v3/steve/election.py`
+
+| Line(s) | Function | Security Relevance | Status |
+|---------|----------|-------------------|--------|
+| ~63-82 | `delete()` | Asserts is_editable, uses transaction | ✓ State checked |
+| ~84-96 | `open()` | Asserts is_editable | ✓ State checked |
+| ~126-131 | `close()` | Asserts is_open | ✓ State checked |
+| ~149-163 | `get_metadata()` | Filters SALT/OPENED_KEY | ✓ Data filtering |
+| ~170-192 | `add_issue()` | Asserts is_editable, validates vtype | ✓ State + type checked |
+| ~229-240 | `add_vote()` | Checks S_OPEN, queries mayvote (no None check) | ⚠️ Crash-based authz |
+| ~278-294 | `has_voted_upon()` | Checks S_OPEN | ✓ State checked |
+| ~296-314 | `is_tampered()` | Integrity verification | ✓ |
+
+---
+
+## Findings
+
+### Critical Findings
+
+#### ASVS-832-SEV-001 — `owner_pid` and `authz` Authorization Values Exist But Are Never Consulted
+
+**Gap Type:** Type B — Control EXISTS but NOT CALLED
+
+**Severity:** CRITICAL
+
+**Location:** `v3/server/pages.py` — `load_election` decorator (line ~193), `load_election_issue` decorator (line ~216), and all management endpoints; `v3/steve/election.py` — `get_metadata()` (line ~149)
+
+**Related Functions Checked:**
+- `load_election()` — `pages.py` ~line 193
+- `load_election_issue()` — `pages.py` ~line 216
+- `_set_election_date()` — `pages.py` ~line 99
+- `do_vote_endpoint()` — `pages.py` ~line 308
+- `do_create_endpoint()` — `pages.py` ~line 341
+- `do_open_endpoint()` — `pages.py` ~line 371
+- `do_close_endpoint()` — `pages.py` ~line 386
+- `do_add_issue_endpoint()` — `pages.py` ~line 401
+- `do_edit_issue_endpoint()` — `pages.py` ~line 420
+- `do_delete_issue_endpoint()` — `pages.py` ~line 440
+
+**Description:** 
+
+The `owner_pid` and `authz` fields are stored in the `election` table and are clearly intended as authorization decision factors for controlling who may manage an election. However, these values are **never checked** at any endpoint. All management operations only require `R.committer` authentication, meaning any authenticated committer can manage any election. Critically, any change to `owner_pid` or `authz` (the values on which authorization decisions should be made) has **zero effect* [...]
+
+**Authorization Data Flow (broken):**
+```
+owner_pid stored → Election.create() → c_create query → DB
+                                                         ↓
+                      [NEVER READ FOR AUTHORIZATION]     ↓
+                                                         ↓
+authz stored → Election.create() → c_create query → DB  ↓
+                                                         ↓
+ANY committer → @asfquart.auth.require({R.committer}) → load_election → endpoint
+                                        ↑
+                     No owner_pid/authz check anywhere
+```
+
+**Vulnerable Code — `load_election` decorator (`pages.py` ~line 193):**
+```python
+def load_election(func):
+    @functools.wraps(func)
+    async def loader(eid):
+        try:
+            e = steve.election.Election(DB_FNAME, eid)
+        except steve.election.ElectionNotFound:
+            # ...
+            raise_404(T_BAD_EID, result)
+
+        _LOGGER.debug(f'Loaded: {e}')
+
+        ### check authz          # <--- TODO: NEVER IMPLEMENTED
+
+        return await func(e)
+    return loader
+```
+
+**Vulnerable Code — `do_open_endpoint` (`pages.py` ~line 371):**
+```python
+@APP.get('/do-open/<eid>')
+@asfquart.auth.require({R.committer})  ### need general solution
+@load_election
+async def do_open_endpoint(election):
+    result = await basic_info()
+
+    ### check authz              # <--- TODO: NEVER IMPLEMENTED
+
+    pdb = steve.persondb.PersonDB.open(DB_FNAME)
+    election.open(pdb)           # Any committer can open any election
+```
+
+**TODO comments confirming the gap exist at:**
+- `pages.py` `load_election` line ~208: `### check authz`
+- `pages.py` `load_election_issue` line ~228: `### check authz`
+- `pages.py` `_set_election_date` line ~99: `### check authz`
+- `pages.py` `do_vote_endpoint` line ~316: `### check authz`
+- `pages.py` `do_create_endpoint` line ~347: `### check authz`
+- `pages.py` `do_open_endpoint` line ~374: `### check authz`
+- `pages.py` `do_close_endpoint` line ~389: `### check authz`
+- `pages.py` `do_add_issue_endpoint` line ~402: `### check authz`
+- `pages.py` `do_edit_issue_endpoint` line ~420: `### check authz`
+
+**Proof of Concept:**
+```
+# Committer B opens Committer A's election without authorization
+# B is authenticated as any committer, A is the election owner
+
+GET /do-open/ELECTION_ID_OWNED_BY_A HTTP/1.1
+Cookie: session=<committer_B_session>
+
+# Result: Election opens successfully. No owner check.
+
+# Committer B closes it:
+GET /do-close/ELECTION_ID_OWNED_BY_A HTTP/1.1
+Cookie: session=<committer_B_session>
+
+# Result: Election closes. Votes can no longer be cast.
+
+# Committer B deletes issues:
+POST /do-delete-issue/ELECTION_ID_OWNED_BY_A/ISSUE_ID HTTP/1.1
+Cookie: session=<committer_B_session>
+
+# Result: Issue deleted from someone else's election.
+```
+
+**Impact:**
+- Any committer can open, close, or modify any election they did not create
+- Changes to `owner_pid` or `authz` values have **no effect** on who can manage elections, directly violating ASVS 8.3.2's requirement that authorization decision values be applied
+- Election integrity compromised: unauthorized users can manipulate election lifecycle
+- Premature close can disenfranchise voters; premature open can lock out issue editing
+
+**Remediation:**
+```python
+def load_election(func):
+    @functools.wraps(func)
+    async def loader(eid):
+        try:
+            e = steve.election.Election(DB_FNAME, eid)
+        except steve.election.ElectionNotFound:
+            result = await basic_info()
+            result.title = 'Unknown Election'
+            result.eid = eid
+            raise_404(T_BAD_EID, result)
+
+        return await func(e)
+    return loader
+
+
+def require_election_owner(func):
+    """Decorator to verify the current user owns the election."""
+    @functools.wraps(func)
+    async def checker(election, *args, **kwargs):
+        s = await asfquart.session.read()
+        md = election.get_metadata()
+        if s['uid'] != md.owner_pid:
+            # Optionally check md.authz for delegated access
+            _LOGGER.warning(
+                f'User[U:{s["uid"]}] denied management of '
+                f'election[E:{election.eid}] owned by {md.owner_pid}'
+            )
+            quart.abort(403, 'You are not authorized to manage this election')
+        return await func(election, *args, **kwargs)
+    return checker
+
+
+@APP.get('/do-open/<eid>')
+@asfquart.auth.require({R.committer})
+@load_election
+@require_election_owner        # <--- ADD THIS
+async def do_open_endpoint(election):
+    # ... existing logic ...
+```
+
+---
+
+### High Findings
+
+#### ASVS-832-SEV-002 — Voter Eligibility Not Explicitly Verified at Vote Submission Endpoint
+
+**Gap Type:** Type C (partial) — Data-layer lookup result not explicitly checked; authorization denial occurs via unhandled crash
+
+**Severity:** HIGH
+
+**Location:** `v3/server/pages.py` — `do_vote_endpoint()` (line ~308); `v3/steve/election.py` — `add_vote()` (line ~229)
+
+**Related Functions Checked:**
+- `do_vote_endpoint()` — `pages.py` ~line 308
+- `add_vote()` — `election.py` ~line 229
+- `vote_on_page()` — `pages.py` ~line 264 (comparison: has explicit check)
+- `q_get_mayvote.first_row()` — invoked in `election.py` ~line 233
+- `q_find_issues.perform()` — invoked in `pages.py` ~line 266
+
+**Description:** 
+
+The vote submission endpoint (`POST /do-vote/<eid>`) does not explicitly verify that the submitting user is an eligible voter for each issue before calling `add_vote()`. While the page-load handler (`vote_on_page`) performs an explicit eligibility check via `q_find_issues`, the submission endpoint skips this check entirely. The `add_vote()` method queries `q_get_mayvote` but never checks the result for `None` — if the voter is ineligible, the code crashes with an `AttributeError` when ac [...]
+
+This violates ASVS 8.3.2 because: if voter eligibility were to change (mayvote entries revoked via direct database modification or future admin feature), the system's response would be an unhandled exception rather than a proper authorization denial with audit logging.
+
+**Data Flow:**
+```
+POST /do-vote/<eid>
+  → @asfquart.auth.require({R.committer})  ← only checks authentication
+  → @load_election                          ← only checks election exists
+  → do_vote_endpoint()
+    → form parsing extracts vote-{iid} pairs
+    → checks iid in issue_dict              ← only checks issue exists in election
+    → election.add_vote(result.uid, iid, votestring)
+      → _all_metadata(self.S_OPEN)          ← checks election is open ✓
+      → q_get_mayvote.first_row(pid, iid)   ← returns None if ineligible
+      → mayvote.salt                        ← CRASHES with AttributeError if None
+    → except Exception as e                 ← caught generically, no authz log
+```
+
+**Vulnerable Code — `add_vote()` (`election.py` ~line 229):**
+```python
+def add_vote(self, pid: str, iid: str, votestring: str):
+    md = self._all_metadata(self.S_OPEN)
+    ### validate VOTESTRING for ISSUE.TYPE voting   # <-- Also not implemented
+    mayvote = self.q_get_mayvote.first_row(pid, iid)
+    # mayvote is None if voter is ineligible — NO CHECK HERE
+    vote_token = crypto.gen_vote_token(md.opened_key, pid, iid, mayvote.salt)  # Crashes
+    ciphertext = crypto.create_vote(vote_token, mayvote.salt, votestring)
+    self.c_add_vote.perform(vote_token, ciphertext)
+```
+
+**Vulnerable Code — `do_vote_endpoint()` (`pages.py` ~line 308):**
+```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                              # <-- Not implemented
+    form = edict(await quart.request.form)
+    # ...
+    for iid, votestring in votes.items():
+        if iid not in issue_dict:                # Only checks issue exists
+            # ...
+        try:
+            election.add_vote(result.uid, iid, votestring)   # No eligibility check
+        except Exception as e:                   # Catches crash generically
+            _LOGGER.error(f'Error adding vote for user[U:{result.uid}] on issue[I:{iid}]: {e}')
+            await flash_danger(f'Error submitting vote for issue {iid}.')
+            # No audit log indicating unauthorized vote attempt
+```
+
+**Contrast with `vote_on_page` which HAS the check (`pages.py` ~line 264):**
+```python
+# Check if user can vote in this election (has mayvote entries)
+election.q_find_issues.perform(result.uid, election.eid)
+if not election.q_find_issues.fetchall():
+    result = await basic_info()
+    result.title = 'Access Denied'
+    result.eid = election.eid
+    raise_404(T_BAD_EID, result)
+```
+
+**Proof of Concept:**
+```
+# Authenticated committer who is NOT a voter on this election
+POST /do-vote/ELECTION_ID HTTP/1.1
+Cookie: session=<non_voter_committer_session>
+Content-Type: application/x-www-form-urlencoded
+
+vote-ISSUE_ID_1=yes
+
+# Result: AttributeError crash, caught generically
+# No proper 403, no audit log of unauthorized attempt
+# Error message: "Error submitting vote for issue ISSUE_ID_1"
+```
+
+**Impact:**
+- Authorization denial relies on crash behavior rather than explicit check — fragile and could break with code refactoring (e.g., adding `if mayvote:` null-safety would remove the only barrier)
+- No meaningful audit logging when unauthorized vote attempts occur
+- Error message to user does not indicate authorization failure (confusing UX)
+- If per-issue eligibility varies, partial votes could be committed before a crash on an ineligible issue
+
+**Remediation:**
+```python
+# In election.py - add_vote()
+def add_vote(self, pid: str, iid: str, votestring: str):
+    md = self._all_metadata(self.S_OPEN)
+
+    mayvote = self.q_get_mayvote.first_row(pid, iid)
+    if mayvote is None:
+        raise VoterNotEligible(pid, iid)  # New explicit exception
+
+    vote_token = crypto.gen_vote_token(md.opened_key, pid, iid, mayvote.salt)
+    ciphertext = crypto.create_vote(vote_token, mayvote.salt, votestring)
+    self.c_add_vote.perform(vote_token, ciphertext)
+
+
+class VoterNotEligible(Exception):
+    def __init__(self, pid, iid):
+        self.pid = pid
+        self.iid = iid
+        super().__init__(f'Person[P:{pid}] not eligible for Issue[I:{iid}]')
+
+
+# In pages.py - do_vote_endpoint()
+@APP.post('/do-vote/<eid>')
+@asfquart.auth.require({R.committer})
+@load_election
+async def do_vote_endpoint(election):
+    result = await basic_info()
+
+    # Explicit eligibility check before processing any votes
+    election.q_find_issues.perform(result.uid, election.eid)
+    if not election.q_find_issues.fetchall():
+        _LOGGER.warning(
+            f'User[U:{result.uid}] attempted unauthorized vote '
+            f'on election[E:{election.eid}]'
+        )
+        quart.abort(403, 'You are not eligible to vote in this election')
+
+    # ... rest of vote processing ...
+```
+
+---
+
+### Medium Findings
+
+#### ASVS-832-SEV-003 — Election State-Changing Operations Use GET Requests, Undermining Authorization State Integrity
+
+**Gap Type:** Type A — Entry point with insufficient protection for authorization-impacting state change
+
+**Severity:** MEDIUM
+
+**Location:** `v3/server/pages.py` — `do_open_endpoint()` (line ~371), `do_close_endpoint()` (line ~386)
+
+**Related Functions Checked:**
+- `do_open_endpoint()` — `pages.py` ~line 371
+- `do_close_endpoint()` — `pages.py` ~line 386
+- `election.open()` — `election.py` ~line 84
+- `election.close()` — `election.py` ~line 126
+
+**Description:** 
+
+Election open and close operations are implemented as GET endpoints. Election state (`editable` → `open` → `closed`) is a critical authorization decision factor — it controls whether voting is accepted, whether issues can be edited, and whether tallying is permitted. Because these state transitions are triggered by GET requests, they can be initiated by embedded image tags, link prefetching, browser extensions, or social engineering (shared link). Combined with the missing owner authoriz [...]
+
+ASVS 8.3.2 requires that changes to authorization decision values be controlled. Using GET for these operations means the authorization state change is trivially triggerable without the user's explicit intent.
+
+**Vulnerable Code — (`pages.py` ~line 370-398):**
+```python
+@APP.get('/do-open/<eid>')                     # <--- GET for state change
+@asfquart.auth.require({R.committer})
+@load_election
+async def do_open_endpoint(election):
+    result = await basic_info()
+    ### check authz
+    pdb = steve.persondb.PersonDB.open(DB_FNAME)
+    election.open(pdb)                          # Changes election state
+    # ...
+
+@APP.get('/do-close/<eid>')                    # <--- GET for state change
+@asfquart.auth.require({R.committer})
+@load_election
+async def do_close_endpoint(election):
+    result = await basic_info()
+    ### check authz
+    election.close()                            # Changes election state
+    # ...
+```
+
+**Proof of Concept:**
+```html
+<!-- Embedded in any page visible to an authenticated committer -->
+<img src="https://steve.example.org/do-close/TARGET_ELECTION_ID" width="0" height="0">
+
+<!-- Or sent as a link: "Check out the election results!" -->
+<!-- Clicking this closes the election -->
+```
+
+**Impact:**
+- Election state can be changed without explicit user intent
+- Closing an open election immediately prevents all further voting (authorization change)
+- Opening an editable election locks out all issue editing (authorization change)
+- Combined with SEV-001 (no owner check), any committer's browser can be weaponized
+
+**Remediation:**
+```python
+@APP.post('/do-open/<eid>')                    # <--- Change to POST
+@asfquart.auth.require({R.committer})
+@load_election
+@require_election_owner
+async def do_open_endpoint(election):
+    # Validate CSRF token
+    form = await quart.request.form
+    # validate_csrf(form.get('csrf_token'))
+    
+    pdb = steve.persondb.PersonDB.open(DB_FNAME)
+    election.open(pdb)
+    # ...
+
+@APP.post('/do-close/<eid>')                   # <--- Change to POST
+@asfquart.auth.require({R.committer})
+@load_election
+@require_election_owner
+async def do_close_endpoint(election):
+    form = await quart.request.form
+    # validate_csrf(form.get('csrf_token'))
+    
+    election.close()
+    # ...
+```
+
+---
+
+## Positive Security Patterns
+
+1. **Real-time state verification at data layer:** `add_vote()` checks `_all_metadata(self.S_OPEN)` which queries the database at execution time, ensuring election state changes are immediately reflected (lines 229-230, `election.py`).
+
+2. **Sensitive data filtering in public APIs:** `get_metadata()` explicitly excludes `SALT` and `OPENED_KEY` (lines 149-163, `election.py`). `get_issue()` never returns the salt. Queries in `queries.yaml` (`q_owned`, `q_open_to_me`) exclude crypto fields.
+
+3. **Parameterized SQL queries throughout:** All queries in `queries.yaml` use `?` placeholders, preventing SQL injection.
+
+4. **Transaction management for multi-step operations:** `delete()` and `add_salts()` use explicit `BEGIN TRANSACTION` / `COMMIT` for atomicity.
+
+5. **Document serving with eligibility check:** `serve_doc()` verifies the requesting user has a `mayvote` entry for the issue before serving documents (lines 498-504, `pages.py`).
+
+6. **Deterministic election integrity verification:** `is_tampered()` recomputes the `opened_key` from current election data and compares with the stored value, providing tamper detection.
+
+7. **Vote anonymization:** Votes are shuffled before tallying (`crypto.shuffle(votes)`), vote tokens are used instead of direct PID/IID pairs, and per-voter salts prevent correlation.
+
+---
+
+## Architecture Observations
+
+1. **Authorization is two-tier but only one tier is implemented:** Authentication (`R.committer`, `R.pmc_member`) is consistently applied via decorators. However, the second tier — operation-level authorization based on `owner_pid` and `authz` — exists only as database fields and TODO comments. This creates a false sense of security, as the authorization infrastructure appears to exist but provides no enforcement.
+
+2. **State-machine enforcement is solid at the data layer:** The Election class properly enforces state transitions (`editable` → `open` → `closed`) via assertions and database queries. This is the correct pattern for ASVS 8.3.2 compliance regarding election state.
+
+3. **No mechanism to revoke voter eligibility during an open election:** The `add_voter()` method asserts `is_editable()`, and there is no `remove_voter()` method. Once an election is open, voter eligibility is immutable. This is actually a positive for ASVS 8.3.2 — the authorization factors can't change, so immediate application is trivially satisfied for voter eligibility.
+
+4. **Error handling masks authorization failures:** The generic `except Exception` in `do_vote_endpoint` catches both legitimate errors and authorization failures identically, preventing proper audit trails.
+
+---
+
+## Recommendations Summary
+
+### Immediate Actions
+
+| Priority | Finding ID | Action | Effort |
+|----------|-----------|--------|--------|
+| **1** | ASVS-832-SEV-001 | Implement `owner_pid`/`authz` checks on all management endpoints via a `require_election_owner` decorator | Medium |
+| **2** | ASVS-832-SEV-002 | Add explicit `None` check in `add_vote()` for `mayvote`, add explicit eligibility pre-check in `do_vote_endpoint`, add `VoterNotEligible` exception with audit logging | Low |
+| **3** | ASVS-832-SEV-003 | Change `/do-open/<eid>` and `/do-close/<eid>` from GET to POST, add CSRF token validation | Low |
+
+### Short-term Actions
+
+| Priority | Action | Effort |
+|----------|--------|--------|
+| **4** | Replace all `### check authz` TODO comments with actual implementations; establish code review policy to prevent TODO-based security gaps from reaching production | Medium |
+| **5** | Implement distinct exception handling for authorization failures vs. operational errors in vote submission, with security-specific audit logging | Low |
+
+### Long-term Actions
+
+- Implement comprehensive authorization audit logging for all management operations
+- Consider adding role-based delegation support using the `authz` field
+- Implement automated security testing to verify authorization controls are enforced
+- Add monitoring/alerting for authorization failures to detect potential attacks
+
+---
+
+## Appendix: Files Analyzed
+
+<details>
+<summary>Click to expand file list (3 files analyzed, 12 skipped)</summary>
+
+### Analyzed Files
+
+1. **`v3/steve/election.py`** (relevance: 9)
+   - Core election management logic
+   - Authorization state enforcement via state machine
+   - Voter eligibility queries
+
+2. **`v3/server/pages.py`** (relevance: 8)
+   - All HTTP endpoints for election management
+   - Authentication decorators
+   - Missing authorization checks
+
+3. **`v3/queries.yaml`** (relevance: 4)
+   - Database schema definitions
+   - Authorization field definitions (`owner_pid`, `authz`)
+   - Voter eligibility queries
+
+### Files Skipped
+
+12 files skipped (not security-relevant for ASVS 8.3.2 authorization decision value enforcement)
+
+</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