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 03:45:23 UTC
(tooling-agents) branch main updated: ASVS L3 audit: 14.2.2 (sensitive_data_handling)
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 a8dd8cd ASVS L3 audit: 14.2.2 (sensitive_data_handling)
a8dd8cd is described below
commit a8dd8cdce82a93548e130d667b2f18d275fe526f
Author: ASF Tooling Team <ro...@apache.org>
AuthorDate: Wed Apr 22 05:45:18 2026 +0200
ASVS L3 audit: 14.2.2 (sensitive_data_handling)
---
.../v3/d0aa7e9/sensitive_data_handling/14.2.2.md | 359 +++++++++++++++++++++
1 file changed, 359 insertions(+)
diff --git a/repos/apache/ASVS/reports/steve/v3/d0aa7e9/sensitive_data_handling/14.2.2.md b/repos/apache/ASVS/reports/steve/v3/d0aa7e9/sensitive_data_handling/14.2.2.md
new file mode 100644
index 0000000..6e841de
--- /dev/null
+++ b/repos/apache/ASVS/reports/steve/v3/d0aa7e9/sensitive_data_handling/14.2.2.md
@@ -0,0 +1,359 @@
+# ASVS 14.2.2 Security Audit Report
+
+## Executive Summary
+
+- **Repository:** apache/steve
+- **Audit Date:** Apr 22, 2026
+- **Auditor:** Tooling Agents
+- **ASVS Requirement:** 14.2.2 — Verify that the application prevents sensitive data from being cached in server components, such as load balancers and application caches, or ensures that the data is securely purged after use.
+ - **Level:** 2
+ - **Section:** General Data Protection
+ - **Section Description:** This section contains various practical requirements related to the protection of data. Most are specific to particular issues such as unintended data leakage, but there is also a general requirement to implement protection controls based on the protection level required for each data item.
+- **Files Analyzed:** 20 relevant out of 20 total, 0 skipped
+
+### Finding Summary
+
+| Severity | Count | Status |
+|----------|-------|--------|
+| Critical | 0 | ✓ None found |
+| High | 1 | ⚠ Requires immediate attention |
+| Medium | 1 | ⚠ Should be addressed |
+| Low | 0 | ✓ None found |
+| **Total** | **2** | |
+
+**Key Finding:** The application completely lacks `Cache-Control` response headers across all endpoints serving sensitive data. No middleware, decorator, or per-route mechanism exists to instruct intermediate server components (load balancers, reverse proxies, CDNs) to avoid caching responses containing voter eligibility data, ballot content, election management information, or user profile details. This is a comprehensive **Type A gap** (entry points with NO control).
+
+---
+
+## Security Controls Inventory
+
+| Control | Defined? | Where Defined | Applied? | Coverage |
+|---------|----------|---------------|----------|----------|
+| Cache-Control headers on responses | **NO** | Nowhere | N/A | **0% — No endpoints protected** |
+| Pragma: no-cache headers | **NO** | Nowhere | N/A | **0%** |
+| After-request middleware for cache headers | **NO** | Nowhere | N/A | **0%** |
+| `<meta>` cache-control in HTML | **NO** | Templates | N/A | **0% — not effective for server caches anyway** |
+| `send_from_directory` cache config | **NO** | `pages.py:serve_doc` | Default (caching enabled) | **Negative — default may encourage caching** |
+
+---
+
+## Critical File Review
+
+### `v3/server/pages.py`
+
+| Function | Serves Sensitive Data | Cache-Control Set | Gap |
+|----------|----------------------|-------------------|-----|
+| `voter_page()` | Yes — voter eligibility | **No** | Type A |
+| `vote_on_page()` | Yes — **ballot content, candidates** | **No** | Type A |
+| `admin_page()` | Yes — election ownership | **No** | Type A |
+| `manage_page()` | Yes — election metadata, issues | **No** | Type A |
+| `manage_stv_page()` | Yes — STV issue data | **No** | Type A |
+| `profile_page()` | Yes — user PII | **No** | Type A |
+| `settings_page()` | Yes — user settings | **No** | Type A |
+| `serve_doc()` | Yes — election documents | **No** (default may enable) | Type A |
+| `do_vote_endpoint()` | Redirect only | **No** | Type A (minor) |
+| `serve_static()` | No (public CSS/JS) | Default | OK |
+| `home_page()` | Minimal (name if logged in) | **No** | Low concern |
+| `about_page()` | No | N/A | OK |
+
+### `v3/server/templates/*.ezt`
+
+| Template | Contains Sensitive Data | Cache Meta Tags | Notes |
+|----------|----------------------|-----------------|-------|
+| `vote-on.ezt` | **Ballot issues, candidates, CSRF token** | None | Highest risk |
+| `voter.ezt` | Election eligibility per user | None | High risk |
+| `admin.ezt` | Election ownership data, CSRF token | None | |
+| `manage.ezt` | Election issues, CSRF token | None | |
+| `manage-stv.ezt` | STV candidates, CSRF token | None | |
+| `header.ezt` | User name/uid in navbar | None | Base template |
+| `home.ezt` | User name if authenticated | None | Low sensitivity |
+
+---
+
+## Findings
+
+### HIGH Severity
+
+#### ASVS-1422-HIGH-001: Complete Absence of Cache-Control Headers on All Sensitive Endpoints
+
+**Severity:** HIGH
+**Gap Type:** Type A — Entry points with NO control
+**File:** `v3/server/pages.py` (affects all route handlers)
+
+**Description:**
+
+Every authenticated endpoint serving sensitive data returns responses without `Cache-Control`, `Pragma`, or `Expires` headers. Per HTTP/1.1 (RFC 7234), responses without explicit cache directives are cacheable by intermediaries. Any load balancer, CDN, or reverse proxy fronting this application may cache and serve authenticated pages to unauthorized users.
+
+**Affected Endpoints (all sensitive):**
+
+| Route | Line | Sensitive Data Exposed |
+|-------|------|----------------------|
+| `GET /voter` | ~137 | Voter eligibility, election list per user |
+| `GET /vote-on/<eid>` | ~220 | **Ballot content**, candidate lists, voter-election mapping |
+| `GET /admin` | ~283 | Owned elections, person data |
+| `GET /manage/<eid>` | ~320 | Election metadata, issue details |
+| `GET /manage-stv/<eid>/<iid>` | ~343 | STV issue data, candidates |
+| `GET /profile` | ~530 | User PII (name, email, uid) |
+| `GET /settings` | ~540 | User settings |
+| `GET /docs/<iid>/<docname>` | ~555 | Authorization-protected election documents |
+
+**Related Functions Checked:**
+- `basic_info()` (line ~60) — Populates base template data, no cache headers set
+- `voter_page()` (line ~137) — Returns voter eligibility data
+- `vote_on_page()` (line ~220) — Returns ballot content
+- `admin_page()` (line ~283) — Returns election ownership data
+- `manage_page()` (line ~320) — Returns election metadata
+- `manage_stv_page()` (line ~343) — Returns STV issue data
+- `profile_page()` (line ~530) — Returns user PII
+- `settings_page()` (line ~540) — Returns user settings
+
+**Vulnerable Code:**
+
+```python
+# v3/server/pages.py, line ~60
+async def basic_info():
+ """Return base-level EZT template data."""
+ basic = edict()
+ # ... populates uid, name, email from session ...
+ basic.csrf_token = 'placeholder'
+ return basic
+ # NOTE: No Cache-Control headers set on the response object
+```
+
+```python
+# v3/server/pages.py, line ~220
+@APP.get('/vote-on/<eid>')
+@asfquart.auth.require({R.committer})
+@load_election
+@APP.use_template(TEMPLATES / 'vote-on.ezt')
+async def vote_on_page(election):
+ result = await basic_info()
+ # ... builds ballot with issues, candidates, voter eligibility ...
+ result.issues = issues_stv + issues_yna
+ return result
+ # Response rendered from template, no Cache-Control headers
+```
+
+**Data Flow:**
+1. User requests `/vote-on/<eid>` → authentication verified
+2. Server builds response containing ballot issues, candidate lists, voter-election mapping
+3. Response sent **without `Cache-Control: no-store`**
+4. Intermediate load balancer / CDN may cache the full HTML response
+5. Subsequent request (same URL) may be served from cache, potentially to a different user or after session expiration
+
+**Proof of Concept:**
+
+```http
+# Request from authenticated user
+GET /vote-on/a1b2c3d4e5 HTTP/1.1
+Host: voting.example.org
+Cookie: session=<valid_token>
+
+# Response (observed)
+HTTP/1.1 200 OK
+Content-Type: text/html
+# ← No Cache-Control header
+# ← No Pragma header
+# ← Body contains: ballot issues, candidates, voter eligibility
+
+# A caching reverse proxy (e.g., Varnish, nginx proxy_cache, AWS ALB)
+# stores this response. A subsequent request to the same URL
+# could be served from cache without authentication verification.
+```
+
+**Impact:**
+- **Ballot exposure:** Cached voting pages reveal which issues a voter is eligible for, leaking voter-to-election correlations that undermine ballot secrecy
+- **Proxy cache poisoning:** A shared cache (CDN, corporate proxy) could serve one voter's personalized ballot page to another user
+- **Persistent data exposure:** After logout, cached pages remain in intermediate caches containing user identity and ballot information
+- **CSRF token leakage:** The (placeholder) CSRF token embedded in cached pages could be reused
+
+**Remediation:**
+
+Add an after-request handler to set `Cache-Control: no-store` on all authenticated responses:
+
+```python
+# v3/server/pages.py — Add near the top, after APP initialization
+
+@APP.after_request
+async def set_cache_control(response):
+ """Prevent caching of sensitive responses by server components."""
+ # Check if this is an authenticated/sensitive response
+ # Static assets (CSS/JS) should still be cacheable
+ if response.content_type and 'text/html' in response.content_type:
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
+ response.headers['Pragma'] = 'no-cache'
+ response.headers['Expires'] = '0'
+ return response
+```
+
+Or more granularly, protect all authenticated routes:
+
+```python
+# Alternative: decorator for sensitive routes
+def no_cache(func):
+ @functools.wraps(func)
+ async def wrapper(*args, **kwargs):
+ response = await func(*args, **kwargs)
+ if isinstance(response, quart.Response):
+ response.headers['Cache-Control'] = 'no-store'
+ response.headers['Pragma'] = 'no-cache'
+ return response
+ return wrapper
+```
+
+---
+
+### MEDIUM Severity
+
+#### ASVS-1422-MED-001: Authorization-Protected Documents Served via `send_from_directory` Without Cache Prevention
+
+**Severity:** MEDIUM
+**Gap Type:** Type A — Entry point with NO control
+**File:** `v3/server/pages.py`
+**Function:** `serve_doc`
+**Lines:** ~555-565
+
+**Description:**
+
+The `/docs/<iid>/<docname>` endpoint serves election documents after verifying voter eligibility via the `mayvote` table. However, `quart.send_from_directory()` uses framework defaults which typically set `Cache-Control: public` or include `max-age` based on `SEND_FILE_MAX_AGE_DEFAULT` config. This actively **encourages** intermediate caches to store authorization-protected documents.
+
+**Related Functions Checked:**
+- `serve_doc()` (lines ~555-565) — Serves authorization-protected documents
+- Database query `q_get_mayvote.first_row()` — Verifies voter eligibility
+
+**Vulnerable Code:**
+
+```python
+# v3/server/pages.py, lines ~555-565
+@APP.get('/docs/<iid>/<docname>')
+@asfquart.auth.require
+async def serve_doc(iid, docname):
+ result = await basic_info()
+
+ db = steve.election.Election.open_database(DB_FNAME)
+ row = db.q_get_mayvote.first_row(result.uid, iid)
+ if not row:
+ quart.abort(404)
+
+ ### verify the propriety of DOCNAME.
+
+ # Return per-issue document — no cache headers set
+ return await quart.send_from_directory(DOCSDIR / iid, docname)
+```
+
+**Data Flow:**
+1. Eligible voter requests `/docs/<iid>/ballot_details.pdf`
+2. Authorization check passes (mayvote row exists)
+3. `send_from_directory` returns the file with **default caching headers**
+4. Load balancer / CDN caches the document with the URL as key
+5. Non-eligible user requests same URL → receives cached document (bypassing authorization)
+
+**Impact:**
+- Election documents containing ballot details, candidate information, or voting instructions could be served from cache to unauthorized users
+- Authorization check is bypassed when response is served from an intermediate cache
+
+**Remediation:**
+
+```python
+@APP.get('/docs/<iid>/<docname>')
+@asfquart.auth.require
+async def serve_doc(iid, docname):
+ result = await basic_info()
+
+ db = steve.election.Election.open_database(DB_FNAME)
+ row = db.q_get_mayvote.first_row(result.uid, iid)
+ if not row:
+ quart.abort(404)
+
+ response = await quart.send_from_directory(DOCSDIR / iid, docname)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, private'
+ response.headers['Pragma'] = 'no-cache'
+ response.headers['Expires'] = '0'
+ return response
+```
+
+---
+
+## Positive Security Patterns
+
+| Pattern | Location | Description |
+|---------|----------|-------------|
+| Sensitive field exclusion from API | `election.py:get_metadata()` | Explicitly omits `salt` and `opened_key` from returned metadata |
+| Person salt protection | `persondb.py:get_person()` | Comment and code explicitly exclude salt: "NEVER return person.salt" |
+| Issue salt protection | `election.py:get_issue()` | Returns only `(title, description, type, kv)`, no salt |
+| Vote shuffling | `election.py:tally_issue()` | `crypto.shuffle(votes)` removes database ordering to prevent correlation |
+| Vote content not logged | `pages.py:do_vote_endpoint()` | Logs `User voted on issue` but **never** logs the vote value |
+| Token-based vote storage | `schema.sql:vote` | Votes stored with `vote_token` (hash), not PID — prevents direct voter-vote correlation |
+| Non-sequential IDs | `schema.sql` | 10-hex-char random IDs prevent URL enumeration |
+
+---
+
+## Architecture Observations
+
+1. **No response middleware layer:** The application lacks any `@APP.after_request` handler, meaning there is no centralized point where security headers (Cache-Control, HSTS, X-Content-Type-Options, etc.) can be applied. This is a structural gap that goes beyond caching.
+
+2. **Template rendering via decorator:** The `@APP.use_template()` decorator handles response rendering, but provides no hook for setting response headers. This architectural choice makes it harder to add per-route cache control.
+
+3. **`send_from_directory` for auth-protected content:** Using a static file serving function for authorization-gated content is inherently risky because the function is designed for public static assets and its default caching behavior works against security.
+
+4. **State-changing GET endpoints:** `/do-open/<eid>` and `/do-close/<eid>` use GET method, making them susceptible to caching by proxies that cache GET responses. While the primary concern is CSRF (acknowledged as known debt), from a caching perspective these redirects could also be cached.
+
+---
+
+## Recommendations Summary
+
+### Immediate Actions (Priority 1 - Critical)
+
+| Priority | Action | Effort | Impact | Timeline |
+|----------|--------|--------|--------|----------|
+| **1** | Add `@APP.after_request` handler setting `Cache-Control: no-store` on all `text/html` responses | Low | Fixes Finding ASVS-1422-HIGH-001 for all current and future endpoints | Immediate |
+| **2** | Override cache headers on `serve_doc()` response with `Cache-Control: no-store, private` | Low | Fixes Finding ASVS-1422-MED-001 specifically | Immediate |
+
+### Short-term Actions (Priority 2 - High)
+
+| Priority | Action | Effort | Impact | Timeline |
+|----------|--------|--------|--------|----------|
+| **3** | Add `Vary: Cookie` header to all authenticated responses | Low | Prevents shared caches from serving authenticated content across users | 1-2 weeks |
+| **4** | Consider adding `Cache-Control: public, max-age=86400` to `/static/` route explicitly, to differentiate from sensitive routes | Low | Makes caching policy intentional rather than default | 1-2 weeks |
+
+### Long-term Actions (Priority 3 - Medium)
+
+| Priority | Action | Effort | Impact | Timeline |
+|----------|--------|--------|--------|----------|
+| **5** | Audit Quart's `SEND_FILE_MAX_AGE_DEFAULT` configuration | Low | Ensures static file caching is intentionally configured | 1 month |
+| **6** | Implement comprehensive security header middleware (HSTS, X-Content-Type-Options, CSP) | Medium | Addresses broader security posture beyond caching | 1-3 months |
+| **7** | Review and refactor state-changing GET endpoints to use POST method | Medium | Prevents cache-related issues and CSRF vulnerabilities | 1-3 months |
+
+---
+
+## Appendix: Files Analyzed
+
+<details>
+<summary>Click to expand list of 20 files analyzed (all with relevance score: 10)</summary>
+
+1. `v3/schema.sql`
+2. `v3/server/pages.py`
+3. `v3/server/templates/about.ezt`
+4. `v3/server/templates/admin.ezt`
+5. `v3/server/templates/e_bad_eid.ezt`
+6. `v3/server/templates/e_bad_iid.ezt`
+7. `v3/server/templates/e_bad_pid.ezt`
+8. `v3/server/templates/flashes.ezt`
+9. `v3/server/templates/footer.ezt`
+10. `v3/server/templates/header.ezt`
+11. `v3/server/templates/home.ezt`
+12. `v3/server/templates/manage-stv.ezt`
+13. `v3/server/templates/manage.ezt`
+14. `v3/server/templates/privacy.ezt`
+15. `v3/server/templates/profile.ezt`
+16. `v3/server/templates/settings.ezt`
+17. `v3/server/templates/vote-on.ezt`
+18. `v3/server/templates/voter.ezt`
+19. `v3/steve/election.py`
+20. `v3/steve/persondb.py`
+
+</details>
+
+---
+
+**End of Report**
\ 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