#1485·guardrails

Best-practice: litellm pin excludes patched CVE versions, unverified-jwt-decode duplication, workflow inputs interpolation

Author: elfrostCreated May 19, 2026Updated Aug 18, 2026

Hi Guardrails team,

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

This is the smallest scan in our series so far (17 findings on 43 MB of code, ~3 worth comment, ~4 false positives) — guardrails' core codebase is unusually tight. But it's also the first scan in the series where the dependency-scan layer surfaces something the SAST layer couldn't see, which is the one item worth leading with.

Full write-up with FP analysis: https://elfrost.github.io/ai-patchlab/scans/guardrails-ai-guardrails.html

1. `pyproject.toml` — `litellm<1.82.6` upper bound excludes 7 published CVE/GHSA fixes

pip-audit against the pinned range surfaces seven advisories that have patches in litellm versions above the current upper bound (`<1.82.6`):

  • CVE-2026-35029
  • CVE-2026-35030
  • GHSA-69x8-hrgq-fjj8
  • CVE-2026-42203
  • CVE-2026-42208
  • CVE-2026-42271
  • CVE-2026-40217

The pin currently resolves to a litellm version with all seven applicable. If the upper bound exists because of a known incompatibility with newer litellm, this is the place to document it; if it's a forgotten upper bound, bumping past `1.82.6` (or removing it) clears the entire category in one line.

2. `guardrails/cli/server/hub_client.py:97` + `guardrails/hub_token/token.py:46` — duplicated `jwt.decode(..., verify_signature=False)` block

Both files contain the same function, line-for-line:

```python def get_jwt_token(rc: RC) -> Optional[str]: token = rc.token

# check for jwt expiration
if token:
    try:
        jwt.decode(token, options={\"verify_signature\": False, \"verify_exp\": True})
    except ExpiredSignatureError:
        raise ExpiredTokenError(TOKEN_EXPIRED_MESSAGE)
    except DecodeError:
        raise InvalidTokenError(TOKEN_INVALID_MESSAGE)
return token

```

Safe today — the comment makes the intent clear (client-side expiry check before sending the token to the Hub backend, which validates the signature server-side). But:

  • The code is duplicated in two places, so any future intent or trust-model documentation has to be maintained in both.
  • `jwt.decode(..., verify_signature=False)` is the canonical footgun shape — every static scanner flags it; every contributor's first read of the file pauses on it; any future maintainer who adds `payload = jwt.decode(...)` for any other purpose has already crossed the safety boundary.
  • Guardrails is a security-adjacent project — `unverified-jwt-decode` flagged in your own client library is awkward optics even when the function is correctly used.

The cleanest refactor extracts the common logic and replaces `jwt.decode` with a manual base64 decode of just the expiry claim, so the unverified-signature shape disappears entirely:

```python import base64, json, time

def _client_check_token_expiry(token: str) -> None: """Client-side check that token is not expired.

Does NOT validate the signature — the Guardrails Hub server validates
the signature on every request. This function exists only to fail
fast on locally-known expired tokens.
\"\"\"
try:
    _header, payload_b64, _signature = token.split('.')
    # base64 padding fix
    payload = json.loads(base64.urlsafe_b64decode(payload_b64 + '==='))
except (ValueError, json.JSONDecodeError) as exc:
    raise InvalidTokenError(TOKEN_INVALID_MESSAGE) from exc

exp = payload.get('exp')
if exp is not None and exp < time.time():
    raise ExpiredTokenError(TOKEN_EXPIRED_MESSAGE)

```

Then both call sites import this. No more `jwt.decode(verify_signature=False)` in either file.

3. `.github/actions/validator_pypi_publish/action.yml` — 4× `${{ inputs.* }}` interpolation, one of which interpolates a secret

The repeated pattern is the same shell-injection class resolved upstream by gptme PR #2399 and PraisonAI PR #1677. One occurrence stands out:

```yaml

  • name: Create .pypirc shell: bash run: | ... echo "password = ${{ inputs.guardrails_token }}" >> ~/.pypirc ```

The secret `inputs.guardrails_token` is interpolated directly into an `echo` command. GitHub Actions masks secrets in normal log output, but the masking is content-based — any future transformation in the same step (`tr`, `sed`, base64) defeats it. The defensible pattern across all four sites is to pipe values through `env:` and reference them from the shell:

```yaml

  • name: Create .pypirc shell: bash env: PYPI_URL: ${{ inputs.pypi_repository_url }} GUARDRAILS_TOKEN: ${{ inputs.guardrails_token }} run: | { echo "[distutils]" echo "index-servers =" echo " private-repository" echo "" echo "[private-repository]" echo "repository = $PYPI_URL" echo "username = token" echo "password = $GUARDRAILS_TOKEN" } > ~/.pypirc ```

The other three interpolations in the same action (`:62, :67, :77`) get the same treatment.


Happy to open separate PRs for any of these. Thanks for guardrails — beyond these three items, the scan turned up only false positives (the validator-hub `non-literal-import` calls, which are by-design plugin discovery). The codebase is unusually tight for its scope.

Source: guardrails-ai/guardrails