Skip to content

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

@elfrost

Description

@elfrost

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`):

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions