# How to Enable Ruff Security Rules


[Ruff](https://pydevtools.com/handbook/reference/ruff.md)'s `S` rule group implements [flake8-bandit](https://docs.astral.sh/ruff/rules/#flake8-bandit-s) security checks, catching hardcoded passwords, injection vulnerabilities, insecure hashing, and unsafe deserialization before they reach production.

## Enable the Full Set

Add the `S` group to the project's `pyproject.toml`:

```toml
[tool.ruff.lint]
extend-select = [
    "S",    # flake8-bandit security rules
]
```

This enables every stable flake8-bandit rule. The 13 `S4xx` suspicious-import rules (S401 through S415, which flag `import telnetlib`, `import pickle`, `import subprocess`, and similar) are preview-only and need `preview = true` in the same table.

Run the linter against a file with a hardcoded secret, an MD5 call, and a string-built SQL query:

```python {filename="app.py"}
import hashlib

password = "supersecret123"
digest = hashlib.md5(b"data").hexdigest()


def get_user(user_id):
    query = "SELECT * FROM users WHERE id = " + user_id
    return query
```

```bash
uv run ruff check --output-format concise .
```

```text
app.py:3:12: S105 Possible hardcoded password assigned to: "password"
app.py:4:10: S324 Probable use of insecure hash functions in `hashlib`: `md5`
app.py:8:13: S608 Possible SQL injection vector through string-based query construction
Found 3 errors.
```

Without `--output-format concise`, Ruff prints the same three findings with a source snippet under each.

## Fix What the Linter Finds

Load the secret from an environment variable ([a `.env` file](https://pydevtools.com/handbook/how-to/how-to-manage-environment-variables-in-a-python-project.md) keeps it out of the repository), switch to SHA-256, and parameterize the query:

```python {filename="app.py"}
import hashlib
import os

password = os.environ["APP_PASSWORD"]
digest = hashlib.sha256(b"data").hexdigest()


def get_user(cursor, user_id):
    cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
    return cursor.fetchone()
```

Rerun `uv run ruff check .` and it prints `All checks passed!`.

## Pick Specific Rules

For a more targeted approach, enable individual rules instead of the full group:

```toml
[tool.ruff.lint]
extend-select = [
    "S105",  # Hardcoded passwords in strings
    "S106",  # Hardcoded passwords in function arguments
    "S107",  # Hardcoded passwords in function defaults
    "S108",  # Insecure temp file usage
    "S113",  # HTTP requests without timeout
    "S301",  # Pickle deserialization
    "S324",  # Insecure hash functions
    "S608",  # SQL injection via string formatting
]
```

These rules can be combined with the [recommended Ruff defaults](https://pydevtools.com/handbook/how-to/how-to-configure-recommended-ruff-defaults.md) by adding them to the same `extend-select` list.

| Rule | Category | What it catches |
|------|----------|----------------|
| S105, S106, S107 | Secrets | Hardcoded passwords in assignments, arguments, and defaults |
| S108 | File safety | Insecure use of temp files |
| S113 | Network | HTTP requests missing a `timeout` parameter |
| S301 | Deserialization | Use of `pickle.loads` and related functions |
| S324 | Cryptography | Insecure hash algorithms like MD5 and SHA1 |
| S608 | Injection | SQL queries built with string concatenation or formatting |

## Tune the Rules

Two options under `[tool.ruff.lint.flake8-bandit]` change what the rules flag:

```toml
[tool.ruff.lint.flake8-bandit]
hardcoded-tmp-directory-extend = ["/scratch"]  # S108 flags /scratch as well as /tmp, /var/tmp, /dev/shm
check-typed-exception = true                   # S110 and S112 also flag `except ValueError: pass`
```

By default S110 and S112 only report `except Exception: pass` and `except BaseException: pass`; typed handlers pass silently.

## Suppress False Positives

Some `S` rules flag code that is safe in context. S101 reports every `assert`, so with the full group enabled a test line needs a `# noqa` comment:

```python
assert response.status_code == 200  # noqa: S101
```

For broader suppression, use per-file ignores in `pyproject.toml`. This is common for test files, where `assert` statements (S101) and hardcoded test credentials (S105, S106) are expected:

```toml
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S101", "S105", "S106"]
```

To silence a single finding, see [how to suppress a Ruff warning on one line](https://pydevtools.com/handbook/how-to/how-to-suppress-a-ruff-warning-on-one-line.md); for a region of a file, see [how to disable Ruff rules for a block of code](https://pydevtools.com/handbook/how-to/how-to-disable-ruff-rules-for-a-block-of-code.md).

## Compare Ruff With bandit

Ruff's `S` rules and [bandit](https://github.com/PyCQA/bandit) both inspect one file's syntax tree at a time with no dataflow analysis, so neither traces a tainted value across function calls. bandit attaches a severity and a confidence level to each finding and reads `# nosec` comments; Ruff reports every finding at one level and reads `# noqa`.

Both tools check first-party code only. For vulnerabilities in dependencies, [scan them with pip-audit or uv audit](https://pydevtools.com/handbook/how-to/how-to-scan-python-dependencies-for-vulnerabilities.md) and [harden installs against supply-chain attacks](https://pydevtools.com/handbook/how-to/how-to-protect-against-python-supply-chain-attacks-with-uv.md).
