feature:security

Table of contents

  1. Introduction
  2. Scanning every feature
  3. Fixing SQL
  4. Fixing HTML built in Python
  5. Fixing a template
  6. Marking a reviewed exception
  7. Reference
    1. Rules
    2. What it reads
  8. See also

Introduction

feature:security lists the lines of a feature that build SQL or HTML from a string, and the template code the Content-Security-Policy blocks. The framework escapes what a template prints and binds what the ORM sends, so these are the only ways left to an SQL injection or an XSS, and no running app can catch them. Run it while you work on a feature. product:validate runs the same check on every feature of a product, and feature:release refuses a feature that fails it.

splent feature:security notes
  Security scan (SQL and XSS sinks)

  splent_feature_notes
    routes.py:42 [sql-from-string] text() takes SQL built with an f-string
      Bind the values, text("... WHERE id = :id").bindparams(id=value), or use the ORM.
    templates/notes/index.html:18 [inline-handler] <button onclick=> runs inline code, which the Content-Security-Policy blocks
      Bind the handler in the feature's JavaScript with addEventListener, and find the element by a data- attribute.

  ❌ 2 finding(s) in 1 of 1 feature.

It exits 0 when there is nothing to fix and 1 when there is.


Scanning every feature

splent feature:security

Without a name it scans every editable feature at the workspace root. Name several to scan just those.

splent feature:security notes profile

Fixing SQL

Send values as bound parameters, or use the ORM.

# Refused
db.session.execute(text(f"SELECT id FROM notes WHERE title = '{title}'"))

# Fine
db.session.execute(
    text("SELECT id FROM notes WHERE title = :title"), {"title": title}
)
Note.query.filter(Note.title == title)

An order the request names goes through a mapping of the columns it may pick. contains searches inside a column with the request’s % and _ taken as written.

from splent_framework.security import contains, sort_by

notes = sort_by(
    Note.query,
    request.args.get("sort"),
    {"title": Note.title, "created": Note.created_at},
)
notes = notes.filter(contains(Note.title, request.args.get("q", "")))

A table or column name may come from a constant, a model’s __tablename__ or a mapping your code fixes.

TABLE = "notes"
db.session.execute(text(f"SELECT COUNT(*) FROM {TABLE}"))

Fixing HTML built in Python

Build markup with Markup(...).format(), which escapes each value, or wrap each value in escape().

# Refused
return Markup(f'<a href="{url}">{name}</a>')

# Fine
return Markup('<a href="{}">{}</a>').format(url, name)
return Markup(f'<a href="{escape(url)}">{escape(name)}</a>')

A template you render from a string goes in the feature’s templates/ and through render_template().


Fixing a template

Move the code to the feature’s JavaScript, and find the element by a data- attribute.

<!-- Refused -->
<button onclick="archive({{ note.id }})">Archive</button>
<a href="javascript:void(0)" class="more">More</a>

<!-- Fine -->
<button type="button" data-archive="{{ note.id }}">Archive</button>
<button type="button" class="more">More</button>

A <script> that must stay inline takes the request’s nonce, and data for the page goes in a JSON block.

<script nonce="{{ csp_nonce() }}">document.documentElement.dataset.theme = "dark";</script>
<script type="application/json" id="notes-config">{{ config | tojson }}</script>

Never turn escaping off with autoescape false. HTML your code builds is Markup in Python, and | safe cleans any other string as rich text.


Marking a reviewed exception

When you have reviewed a line and it is safe, such as a table name your code quotes, say so on that line with the rule’s name.

conn.execute(text(f"DROP TABLE {quote(name)}"))  # splent: allow sql-from-string
<a href="javascript:history.back()">Back</a>{# splent: allow javascript-url #}

In Python the comment holds on any line of the statement, so it stays valid wherever ruff format moves it. Of an if, with or def only the first line counts. In a template it holds on its own line, or on the line the tag starts. Several rules go comma separated.


Reference

Argument Description
FEATURE_IDENTIFIERS Short names (notes) or package names (splent_feature_notes), each optionally under a namespace (splent-io/notes) or with @<version>. None scans every editable feature at the workspace root

Rules

Rule Finding
sql-from-string The SQL of text(), execute(), exec_driver_sql() or literal_column(), or a string given to filter(), where(), order_by() or having(), built with an f-string, %, + or format() from a value that is not a literal
markup-from-string Markup() of a string built that way from a value not wrapped in escape()
template-from-string render_template_string() of a template that is not a literal
autoescape-off {% autoescape false %}
inline-handler An event handler attribute, such as onclick= or onsubmit=
inline-script A <script> with code and no src or nonce. A data block, such as type="application/json" or type="application/ld+json", is fine
javascript-url An href, src, action or formaction that starts with javascript:

A literal, a module-level constant, a pick from a constant mapping and a local name only ever bound to one of those are not variable. A plain parameter passed through is not reported, only a string built in the feature.

What it reads

It reads the copy a name resolves to, the working copy at the workspace root first, then a snapshot in .splent_cache. Inside the package it reads every .py file and the .html, .j2, .xml and .svg files under templates/. Tests are left out, and a file that does not parse is left to the linter.

Message Meaning
Feature '<id>' not found at the workspace root or in .splent_cache. No copy to read. Exits 1

See also


Back to top

splent. Distributed by an LGPL license v3. Contact us: drorganvidez@us.es