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.
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
```
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 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.
```
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
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
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.