# Risk map — pallets/flask

> Canonical page: https://codevetter.com/unpack/flask/risk-map

# Risk map — pallets/flask

Scanned commit d73fa1cdcbd8b1465c151db8924ba58b1dd14e35 (2026-09-08). Generated by codevetter codevetter 1.12.0 on 2026-09-22.

Flask's security-sensitive surface is narrow but load-bearing: signed session cookies, host validation, file serving, template autoescaping, and code that executes user-supplied Python. The largest structural risk in this checkout is the 3.2 context merge and the `ctx`-first-parameter signature change, which is backstopped only by a signature-sniffing `__init_subclass__` shim that mutates the base `Flask` class at subclass-definition time. Config loading (`from_pyfile`), the shell command (`PYTHONSTARTUP`), and CLI app discovery (`--cert` import strings, factory `ast.literal_eval`) all evaluate code or import arbitrary modules by design. Several defaults are permissive by intent and must be set by deployers: `SESSION_COOKIE_SECURE=False`, `SESSION_COOKIE_SAMESITE=None`, `SECRET_KEY=None`, `TRUSTED_HOSTS=None`.
- `Flask.__init_subclass__` mutates global class state: when it detects an old-signature override it calls `setattr(Flask, method.__name__, add_ctx(base_method))` on the *base* class, so one legacy subclass anywhere in the process rewraps `Flask`'s methods for every app in that process. — [src/flask/app.py#L255-L310](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/app.py#L255-L310) [src/flask/app.py#L86-L107](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/app.py#L86-L107)
- `Config.from_pyfile` compiles and `exec`s the target file into a synthetic module namespace — arbitrary code execution by design, so config paths must never come from untrusted input. — [src/flask/config.py#L187-L217](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/config.py#L187-L217)
- `flask shell` executes `PYTHONSTARTUP` via `eval(compile(f.read(), startup, "exec"), ctx)` before the REPL starts, inheriting whatever that env var points at. — [src/flask/cli.py#L1017-L1024](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/cli.py#L1017-L1024)
- `CertParamType.convert` falls back to `werkzeug.utils.import_string(value, silent=True)` for `--cert`, so a non-path `--cert` value imports an arbitrary dotted module attribute looking for an `ssl.SSLContext`. — [src/flask/cli.py#L780-L826](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/cli.py#L780-L826)
- `find_app_by_string` parses `module:factory(args)` with `ast.parse` and `ast.literal_eval` — literal-only, so it resists arbitrary expression injection — but `locate_app` still `__import__`s the module, executing it fully. — [src/flask/cli.py#L120-L196](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/cli.py#L120-L196) [src/flask/cli.py#L241-L265](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/cli.py#L241-L265)
- `open_resource` and `open_instance_resource` join user-supplied `resource` onto `root_path`/`instance_path` with plain `os.path.join` and no `safe_join`, so an absolute or `..` path escapes the directory — safe for the intended developer-controlled use, unsafe if fed request data. — [src/flask/app.py#L415-L468](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/app.py#L415-L468) [src/flask/blueprints.py#L104-L127](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/blueprints.py#L104-L127)
- By contrast `send_from_directory` explicitly documents and delegates to Werkzeug's `safe_join`, and the docstring warns that the `directory` argument itself must never come from the client. — [src/flask/helpers.py#L543-L585](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/helpers.py#L543-L585)
- Security-relevant defaults ship permissive: `SECRET_KEY=None`, `SECRET_KEY_FALLBACKS=None`, `SESSION_COOKIE_SECURE=False`, `SESSION_COOKIE_SAMESITE=None`, `TRUSTED_HOSTS=None`, `MAX_CONTENT_LENGTH=None` — with no secret key, `get_signing_serializer` returns None and sessions silently degrade to `NullSession`. — [src/flask/app.py#L207-L239](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/app.py#L207-L239) [src/flask/sessions.py#L303-L312](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/sessions.py#L303-L312) [src/flask/ctx.py#L381-L393](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/ctx.py#L381-L393)
- Session cookie signing defaults to SHA-1 HMAC via `_lazy_sha1`, deferred to runtime specifically so FIPS builds without SHA-1 can fail at configure time rather than import time. — [src/flask/sessions.py#L276-L300](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/sessions.py#L276-L300)
- `open_session` swallows `BadSignature` and returns an empty session, so tampered or key-rotated-out cookies are indistinguishable from no cookie at the application layer — no signal is emitted. — [src/flask/sessions.py#L323-L336](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/sessions.py#L323-L336)
- Teardown error handling changed to collect-then-raise: `_CollectErrors.raise_any` raises a `BaseExceptionGroup` on Python 3.11+ but only `self.errors[0]` on 3.10, so identical failures surface as different exception types across supported versions. — [src/flask/helpers.py#L654-L682](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/helpers.py#L654-L682) [pyproject.toml#L22](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/pyproject.toml#L22)
- Dead/stale developer config: `.devcontainer/on-create-command.sh` runs `pip install -r requirements/dev.txt`, but no `requirements/` directory exists — the repo moved to uv dependency groups in `pyproject.toml`, so devcontainer creation fails. — [.devcontainer/on-create-command.sh#L1-L7](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/.devcontainer/on-create-command.sh#L1-L7) [.devcontainer/devcontainer.json#L1-L16](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/.devcontainer/devcontainer.json#L1-L16) [pyproject.toml#L36-L68](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/pyproject.toml#L36-L68)
- Deprecated-but-live code paths that must be kept working through 4.0: module-level `__getattr__` shims for `flask.ctx.RequestContext` and `flask.globals.request_ctx`, and the `should_ignore_error` hook checked on every first request in `full_dispatch_request` and again in `wsgi_app`'s finally block. — [src/flask/ctx.py#L528-L540](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/ctx.py#L528-L540) [src/flask/globals.py#L65-L77](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/globals.py#L65-L77) [src/flask/app.py#L995-L1014](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/app.py#L995-L1014) [src/flask/sansio/app.py#L928-L938](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/sansio/app.py#L928-L938)
- Blast-radius hotspot: `src/flask/app.py` (1628 lines) and `src/flask/sansio/app.py` (1013 lines) together hold dispatch, error handling, URL building, response coercion and every registration method — nearly every behavioural change lands in one of these two files. — [src/flask/app.py](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/app.py) [src/flask/sansio/app.py](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/sansio/app.py)
- Fragile coupling to Werkzeug internals: `create_url_adapter` comments that it must call `get_host` itself "until bind_to_environ does", and forces `subdomain` to the default because "Werkzeug doesn't implement subdomain matching yet" — both break silently if Werkzeug changes. (inferred) — [src/flask/app.py#L529-L552](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/app.py#L529-L552)
- `Blueprint.get_send_file_max_age` and `send_static_file` are acknowledged duplicates of the `Flask` methods ("Note this is a duplicate of the same method in the Flask class"), so fixes must be applied in two places. — [src/flask/blueprints.py#L55-L102](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/blueprints.py#L55-L102) [src/flask/app.py#L366-L414](https://github.com/pallets/flask/blob/d73fa1cdcbd8b1465c151db8924ba58b1dd14e35/src/flask/app.py#L366-L414)

Every claim links to its cited files at the pinned commit. Citations identify source evidence; they do not establish runtime correctness. Independent CodeVetter scan — not reviewed by upstream maintainers.

## Public product links

- [CodeVetter](https://codevetter.com/)
- [Download](https://codevetter.com/download)
- [Documentation](https://codevetter.com/docs/)
- [Source](https://github.com/Codevetter/codevetter)
