#596·Upsonic

Best-practice: SSL context, SQLAlchemy text(f''), agent shell=True comment, and pickle persistence

Author: elfrostCreated May 15, 2026Updated May 15, 2026

Hi Upsonic team,

While testing AI PatchLab (an open-source local-first SAST/SCA scanner) on a few mid-popularity Python AI projects, I scanned Upsonic at `1c61f94` and wanted to flag four best-practice items. Filing as a single courtesy issue.

Full curated write-up — including FP analysis, methodology, and the findings the scanner got wrong (the safety-engine test fixtures, the SHA-1 used as a short identifier, etc.) — is at: https://elfrost.github.io/ai-patchlab/scans/upsonic-upsonic.html

1. Global SSL verification disabled in `src/upsonic/ocr/layer_1/engines/easyocr.py:119`

```python import ssl original_context = ssl._create_default_https_context try: ssl._create_default_https_context = ssl._create_unverified_context # ... EasyOCR Reader init that downloads models ... finally: ssl._create_default_https_context = original_context ```

Two issues here:

  • Global side effect: assigning `ssl._create_default_https_context` affects HTTPS verification for the entire Python process until the `finally` restores it. Any other HTTPS call running concurrently (telemetry, LLM API, vector-DB sync) inherits the unverified context for the duration.
  • Window during exception: if an exception happens between the assignment and the `finally`, the restoration will run, but any threaded HTTPS call during the window uses the unverified context.

The cleanest fix is to scope SSL disablement to just the download, not the global module attribute. Even better: pre-download EasyOCR models during deployment using `model_storage_directory` (already supported in the same code path) so the runtime never needs to hit the network with verification off.

2. Eight `text(f"...")` interpolations in `src/upsonic/vectordb/providers/pgvector.py`

Lines 1242, 1247, 1364, 1378, 1417, 1442, 1494 — examples:

```python text(f'CREATE SCHEMA IF NOT EXISTS "{self.schema_name}";') text(f"SET LOCAL ivfflat.probes = {nprobe}") ```

  • The numeric `SET LOCAL` calls (`nprobe`, `ef_search`, etc.) interpolate Pydantic-validated integers from `IVFIndexConfig` / `HNSWIndexConfig`, so SQL injection is gated by the type-validation today. Low realistic risk.
  • The `CREATE SCHEMA` call interpolates `self.schema_name` — a string. PostgreSQL identifier rules limit damage at the DB level, but the application doesn't enforce a stricter allowlist before interpolation.

The defensible fix across all sites is SQLAlchemy's `bindparams()` or `quoted_name()` instead of f-strings, even for "obviously safe" config values. Reasons: keeps the static-analysis surface clean, future-proofs against schema-name sources that could change, and removes the visual ambiguity for code reviewers.

(The `text("CREATE EXTENSION IF NOT EXISTS vector;")` at line 444 is a false positive from the scanner — that's a literal string, no interpolation.)

3. `subprocess.run(command, shell=True)` in `ralph/backpressure/gate.py:268` and `ralph/tools/filesystem.py:434` — a one-line comment

Both are clearly intentional — `filesystem.py` is the agent's shell-execution tool, `gate.py` runs validation/test commands. The trust model is "the user controls the agent; the agent runs shell as the user", same pattern as e.g. gptme's `context_cmd.py`.

Suggestion: a one-line comment above each `subprocess.run` documenting the intentional `shell=True`:

```python

shell=True is intentional: this is the agent's shell-execution tool,

invoked with a command the agent built. Trust boundary: user controls

the agent; do not pass strings from any other source.

result = subprocess.run(command, shell=True, ...) ```

Stops scanners from re-flagging the line, signals to contributors that the trust model has been considered.

4. Pickled persistence in `src/upsonic/graphv2/cache.py` and `checkpoint.py` — sign or migrate

Five `pickle` sites in the graph cache and checkpoint subsystems write/read state to a local SQLite DB. As written, the security posture is "if an attacker can write to the SQLite file, they can achieve arbitrary code execution at the next `pickle.loads`". Acceptable for single-user local deployments; risky for any shared-host or multi-tenant deployment.

Two notes:

  • `cache.py:50` is actually `pickle.dumps` used only to compute a content hash (then SHA-256'd) — that's a false positive, since `dumps` doesn't execute code; only `loads` does. So really four sites, not five.
  • The four `pickle.loads` sites accept whatever blob is in the local SQLite. If two processes share access to the file, the first to put a malicious blob in gets code execution on the next load.

Standard hardening, in order of effort:

  1. Add HMAC to the blob, verify on load (cheapest; keeps pickle).
  2. Migrate to JSON for everything that doesn't need pickled Python objects — most checkpoint state is dict-shaped.
  3. Document the threat model: "pickle is used for local cache; do not deploy this code path in multi-tenant configurations without HMAC or migration."

Happy to open separate PRs for any of these. None blocks anything; these are the items I'd surface during a careful security-review pass.

Thanks for Upsonic — the rest of the 40 findings turned out to be either false positives or by-design patterns. The scanner picked up your own safety policy engine's test fixtures eight times (it sees crafted secrets in detector tests and panics) — that's the kind of FP class that's interesting to document because every project with a built-in detector hits it.