Skip to content

fix(security): harden JWT validation, GCS path handling, and CI workflows#40

Open
williaby wants to merge 5 commits into
mainfrom
claude/security-hardening-jwt-gcs-pDrjq
Open

fix(security): harden JWT validation, GCS path handling, and CI workflows#40
williaby wants to merge 5 commits into
mainfrom
claude/security-hardening-jwt-gcs-pDrjq

Conversation

@williaby
Copy link
Copy Markdown
Contributor

Summary

Security-focused hardening pass covering JWT auth, GCS utilities, GitHub Actions, and dependency posture. No new features — only tightened defaults and removed footguns.

JWT validator — packages/cloudflare-auth/src/cloudflare_auth/validators.py

  • Enforce an explicit allowlist of asymmetric algorithms (RS* / ES* / PS*) at constructor and against the unverified token header. none and any HS* are rejected to block algorithm-confusion and signature-stripping attacks.
  • jwt.decode options are now spelled out and non-bypassable: verify_signature, verify_exp, verify_nbf, verify_iat, plus a require set for exp/iat/iss/sub/aud. 30s leeway is allowed for clock skew.
  • Removed the verify_exp=False keyword from validate_token / validate_token_async — no caller used it and keeping it was a footgun. Callsites in middleware.py / middleware_enhanced.py already pass positionally.
  • Replaced the blanket except Exception with PyJWT-specific handlers and an InvalidTokenError catch-all so programmer errors are no longer mapped to ValueError("Token validation failed").
  • get_unverified_claims now logs a warning on every call so production misuse is visible.

GCS client — packages/gcs-utilities/src/gcs_utilities/client.py

  • _sanitize_gcs_path now rejects control characters, backslashes, and ./.. as path segments (plus any .. substring).
  • _validate_local_path rejects null bytes in local paths.
  • upload_directory re-sanitizes the joined path so symlinks / unusual filenames in the walked tree cannot reintroduce traversal sequences.
  • delete_directory rejects empty / whitespace-only prefixes — empty prefixes would otherwise match every object in the bucket.
  • Bare except Exception in _get_or_create_bucket replaced with the specific GoogleCloudError / OSError / ValueError set.

GitHub Actions — .github/workflows/*.yml

  • All 12 ByronWilliamsCPA/.github/...@main reusable-workflow references pinned to commit e8fc83c98c2971ad1ece71573d28171463e30c16.
  • Workflow-level permissions: reduced to contents: read; elevated rights moved to the specific jobs that need them (release.release, docs.docs, scorecard.scorecard, dependency-review, publish-artifact-registry.publish).
  • step-security/harden-runner@a5ad31d6... with egress-policy: audit added to every job with inline steps that lacked it: 5 jobs in ci.yml, 2 in release.yml, plus dependency-review.yml, publish-artifact-registry.yml, and codecov.yml's failure reporter. codeql.yml, slsa-provenance.yml, and pr-validation.yml already had it.

Dependencies — SECURITY-NOTES.md (new)

  • Flags that every dep in pyproject.toml and packages/*/pyproject.toml uses an unbounded >= floor (no <X.0.0 ceiling).
  • Names the three highest-risk dependencies with rationale and suggested upper bounds:
    1. pyjwt — sole auth gate; PyJWT has shipped behaviour-changing point releases (e.g. CVE-2022-29217).
    2. cryptography — substrate for JWT signature verification and TLS; FIPS compatibility tracks the OpenSSL build.
    3. google-cloud-storage — entire data plane for GCSClient; ADC / credential-resolution defaults have shifted across minors.

Test plan

  • ruff check clean on edited files
  • basedpyright 0 errors on edited files
  • 111 cloudflare-auth tests pass (pytest packages/cloudflare-auth/tests/)
  • 1 gcs-utilities placeholder test passes
  • All 19 workflow YAMLs parse with yaml.safe_load
  • Manual verification of the JWT algorithm allowlist (none / HS256 constructor rejection)
  • Manual verification of GCS path sanitization (traversal, control chars, backslash, empty)
  • Manual verification that delete_directory rejects empty / whitespace / traversal prefixes
  • Reviewer to confirm the chosen reusable-workflow SHA (e8fc83c...) matches the desired pinned state for ByronWilliamsCPA/.github
  • Decide whether to apply the suggested <X.0.0 upper bounds from SECURITY-NOTES.md in this PR or a follow-up

https://claude.ai/code/session_01LfuLSQf3rcbYG1QdgL92a5


Generated by Claude Code

JWT validator (cloudflare-auth):
- Enforce an explicit allowlist of asymmetric algorithms (RS*/ES*/PS*) at
  construction and again against the unverified token header, rejecting
  "none" and HS* to block algorithm-confusion / signature-stripping.
- Make signature/exp/nbf/iat verification non-optional, add a "require"
  set for exp/iat/iss/sub/aud, and add 30s leeway for clock skew.
- Replace blanket "except Exception" with PyJWT-specific exception
  handlers (InvalidTokenError catch-all) and log get_unverified_claims
  use loudly so misuse is visible.

GCS client (gcs-utilities):
- _sanitize_gcs_path now rejects control characters, backslashes, and
  "."/".." as path segments (plus any ".." substring).
- Re-sanitize joined paths in upload_directory so symlink-introduced
  traversal sequences cannot bypass the prefix check.
- delete_directory rejects empty / whitespace-only prefixes to prevent
  accidental bucket-wide wipes.
- Replace bare "except Exception" in _get_or_create_bucket with the
  specific GoogleCloudError / OSError / ValueError set.

GitHub workflows:
- Pin every ByronWilliamsCPA/.github reusable-workflow reference to a
  commit SHA instead of @main.
- Set workflow-level "permissions: contents: read" and grant elevated
  permissions only on the jobs that need them.
- Add step-security/harden-runner with egress-policy: audit to every
  job that runs inline steps and did not already have it.

Dependency review:
- Add SECURITY-NOTES.md flagging that every dependency uses an unbounded
  ">=" floor and naming pyjwt, cryptography, and google-cloud-storage as
  the three highest-risk dependencies with rationale and suggested
  upper-bound constraints.

https://claude.ai/code/session_01LfuLSQf3rcbYG1QdgL92a5
Copilot AI review requested due to automatic review settings May 17, 2026 05:46
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

Warning

Rate limit exceeded

@williaby has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 39 minutes and 51 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0e10594f-ee58-45f7-9d24-eaa325476437

📥 Commits

Reviewing files that changed from the base of the PR and between c0eb24a and 9b9f052.

📒 Files selected for processing (21)
  • .github/workflows/ci.yml
  • .github/workflows/codecov.yml
  • .github/workflows/coverage.yml
  • .github/workflows/dependency-review.yml
  • .github/workflows/docs.yml
  • .github/workflows/fips-compatibility.yml
  • .github/workflows/mutation-testing.yml
  • .github/workflows/publish-artifact-registry.yml
  • .github/workflows/python-compatibility.yml
  • .github/workflows/qlty.yml
  • .github/workflows/release.yml
  • .github/workflows/reuse.yml
  • .github/workflows/sbom.yml
  • .github/workflows/scorecard.yml
  • .github/workflows/security-analysis.yml
  • .github/workflows/slsa-provenance.yml
  • CHANGELOG.md
  • SECURITY-NOTES.md
  • packages/cloudflare-auth/src/cloudflare_auth/validators.py
  • packages/cloudflare-auth/tests/test_validators.py
  • packages/gcs-utilities/src/gcs_utilities/client.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/security-hardening-jwt-gcs-pDrjq

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 17, 2026

Codecov Report

❌ Patch coverage is 81.57895% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
.../cloudflare-auth/src/cloudflare_auth/validators.py 81.57% 7 Missing ⚠️

📢 Thoughts on this report? Let us know!

claude added 2 commits May 17, 2026 05:50
PyJWT's ``jwt.decode(..., algorithms=[...])`` already enforces the
algorithm allowlist by reading the token header internally and raising
``InvalidAlgorithmError`` on mismatch. Combined with the
constructor-time validation of ``settings.jwt_algorithm`` against
``_ALLOWED_JWT_ALGORITHMS``, the explicit ``_reject_unsafe_header_algorithm``
helper added in the previous commit was redundant. Removing it also
removes a ``jwt.get_unverified_header()`` call that SonarCloud and
CodeQL flag as a JWT-without-verification pattern, even though it was
used defensively before the verified decode.

No change in observable behaviour: tokens with unsafe ``alg`` values
are still rejected (now via ``InvalidAlgorithmError`` -> ``ValueError``
in the existing exception handler).

https://claude.ai/code/session_01LfuLSQf3rcbYG1QdgL92a5
Adds the missing test module for ``CloudflareJWTValidator``. Covers:

* Constructor algorithm allowlist accepts every entry in
  ``_ALLOWED_JWT_ALGORITHMS`` and rejects ``none``, HS*, lowercase
  variants, and unknown values.
* ``is_configured`` reflects domain + audience presence; missing
  team domain warns and leaves ``jwks_client`` as ``None``.
* ``validate_token`` raises ``RuntimeError`` when no JWKS client is
  configured rather than silently passing.
* ``_validate_required_claims`` enforces email/iss/aud/sub/iat/exp.
* ``get_unverified_claims`` always logs a warning, returns the
  decoded claims for a parseable token, and returns ``{}`` for
  garbage input.
* ``refresh_keys`` swaps in a new PyJWKClient.
* ``validate_token`` passes ``verify_signature``/``verify_exp``/
  ``verify_nbf``/``verify_iat``/``require`` and an explicit
  ``algorithms`` list to ``jwt.decode``, plus non-zero leeway.
* Each PyJWT-specific exception is mapped to ``ValueError`` with a
  descriptive message.

Brings patch coverage on ``validators.py`` from 13% up so the
codecov/patch gate clears for this PR.

https://claude.ai/code/session_01LfuLSQf3rcbYG1QdgL92a5
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Security hardening pass for JWT validation, GCS path handling, GitHub Actions supply-chain posture, and dependency risk documentation.

Changes:

  • Tightens JWT algorithm and claim validation behavior.
  • Adds stricter GCS path/local path validation and safer directory deletion defaults.
  • Pins reusable workflows, adjusts selected permissions, adds harden-runner steps, and documents dependency version-bound risks.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
SECURITY-NOTES.md Adds dependency risk notes and hardening summary.
packages/cloudflare-auth/src/cloudflare_auth/validators.py Hardens JWT validation and exception handling.
packages/gcs-utilities/src/gcs_utilities/client.py Strengthens path sanitization and deletion safeguards.
.github/workflows/ci.yml Adds harden-runner and narrows job permissions.
.github/workflows/codecov.yml Pins reusable workflow and hardens inline failure job.
.github/workflows/coverage.yml Pins Qlty coverage reusable workflow.
.github/workflows/dependency-review.yml Moves PR permission to job scope and adds harden-runner.
.github/workflows/docs.yml Pins docs workflow and scopes deploy permissions to job.
.github/workflows/fips-compatibility.yml Pins FIPS reusable workflow.
.github/workflows/mutation-testing.yml Pins mutation-testing reusable workflow.
.github/workflows/publish-artifact-registry.yml Moves OIDC permission to publish job and adds harden-runner.
.github/workflows/python-compatibility.yml Pins compatibility reusable workflow.
.github/workflows/qlty.yml Pins Qlty reusable workflow.
.github/workflows/release.yml Narrows workflow permissions and hardens release jobs.
.github/workflows/reuse.yml Pins REUSE reusable workflow.
.github/workflows/sbom.yml Pins SBOM reusable workflow.
.github/workflows/scorecard.yml Moves scorecard permissions to job scope and pins workflow.
.github/workflows/security-analysis.yml Pins security analysis reusable workflow.
.github/workflows/slsa-provenance.yml Pins SLSA reusable workflow.
Comments suppressed due to low confidence (1)

packages/cloudflare-auth/src/cloudflare_auth/validators.py:268

  • After replacing the blanket exception handler, token-controlled validation failures that are not InvalidTokenError can escape this method. For example, PyJWKClientError from get_signing_key_from_jwt() (unknown kid/JWKS issues) or Pydantic ValidationError from CloudflareJWTClaims(**payload) will bypass the ValueError path that the middleware handles and become a 500 instead of an authentication failure.
            logger.warning("JWT validation failed: %s", str(e))
            msg = "Token validation failed"
            raise ValueError(msg) from e

    def _validate_required_claims(self, payload: dict[str, Any]) -> None:
        """Validate that required claims are present.

Comment on lines 193 to +195
)

# Validate required claims
# Validate required claims (defence-in-depth on top of "require")
Comment on lines +193 to 197
except GCSNotFoundError:
raise
except (GoogleCloudError, OSError, ValueError) as e:
msg = f"Failed to access bucket '{self.bucket_name}': {e}"
raise GCSAuthError(msg) from e
# traversal sequences -- defence in depth.
try:
gcs_path = self._sanitize_gcs_path(
f"{gcs_prefix.rstrip('/')}/{rel_path}"
Comment on lines +281 to +284
# Reject ``..`` as a path segment to block traversal. We check
# segments rather than substring so legitimate names like
# ``v1..1`` (no slash) are still rejected (safer default) while
# ``my..file`` callers get a clear error and can rename.
jobs:
security:
uses: ByronWilliamsCPA/.github/.github/workflows/python-security-analysis.yml@main
uses: ByronWilliamsCPA/.github/.github/workflows/python-security-analysis.yml@e8fc83c98c2971ad1ece71573d28171463e30c16 # main
sbom:
name: SBOM & Security
uses: ByronWilliamsCPA/.github/.github/workflows/python-sbom.yml@main
uses: ByronWilliamsCPA/.github/.github/workflows/python-sbom.yml@e8fc83c98c2971ad1ece71573d28171463e30c16 # main
# Skip on forks (no PR comment permissions)
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: ByronWilliamsCPA/.github/.github/workflows/python-mutation.yml@main
uses: ByronWilliamsCPA/.github/.github/workflows/python-mutation.yml@e8fc83c98c2971ad1ece71573d28171463e30c16 # main
jobs:
fips-check:
uses: ByronWilliamsCPA/.github/.github/workflows/python-fips-compatibility.yml@main
uses: ByronWilliamsCPA/.github/.github/workflows/python-fips-compatibility.yml@e8fc83c98c2971ad1ece71573d28171463e30c16 # main
name: SLSA Level 3
needs: [build]
uses: ByronWilliamsCPA/.github/.github/workflows/python-slsa.yml@main
uses: ByronWilliamsCPA/.github/.github/workflows/python-slsa.yml@e8fc83c98c2971ad1ece71573d28171463e30c16 # main
jobs:
reuse:
uses: ByronWilliamsCPA/.github/.github/workflows/python-reuse.yml@main
uses: ByronWilliamsCPA/.github/.github/workflows/python-reuse.yml@e8fc83c98c2971ad1ece71573d28171463e30c16 # main
JWT validator:
- ``validate_token`` now refuses to run when ``cloudflare_audience_tag``
  is unset rather than silently disabling audience verification, so a
  deployment with team domain set but no audience tag can no longer
  accept any token from that issuer.
- Decode options for ``verify_aud`` / ``verify_iss`` are now
  unconditionally True (the new precondition makes the boolean toggle
  unnecessary).
- Add specific handlers for ``PyJWKClientError`` (raised by
  ``get_signing_key_from_jwt`` on unknown ``kid`` / JWKS failures) and
  Pydantic ``ValidationError`` (raised by ``CloudflareJWTClaims(**payload)``
  on malformed claims). Both now surface as ``ValueError`` so the
  middleware returns 401 instead of 500.

GCS client:
- Widen the bucket-access except clause to include
  ``google.auth.exceptions.GoogleAuthError`` so
  ``DefaultCredentialsError`` / ``RefreshError`` continue to be wrapped
  as ``GCSAuthError`` after the BLE001 cleanup.
- ``upload_directory`` now joins ``rel_path.as_posix()`` before
  re-sanitizing the GCS path, so directory uploads from Windows do not
  feed backslashes into the sanitizer (which rejects them).
- Update the ``..`` rejection comment to describe the actual stricter
  behaviour (segment check AND substring check).

Workflows -- move elevated permissions off workflow scope onto the
specific jobs that need them so the workflow-level scope can stay at
``contents: read``:
- ``fips-compatibility.yml`` / ``mutation-testing.yml``: ``pull-requests: write``
  moved to job.
- ``sbom.yml`` / ``security-analysis.yml``: ``security-events: write``
  (and ``pull-requests: write`` for security-analysis) moved to job.
- ``reuse.yml``: replace ``permissions: read-all`` with explicit
  ``contents: read``.
- ``slsa-provenance.yml``: drop workflow-scope ``contents: write``,
  ``id-token: write``, ``actions: read``, ``attestations: write``; the
  ``build`` job now carries the attestation permissions and the ``slsa``
  job already has its own permissions block.

Tests:
- Add tests for the new "audience required" precondition and for the
  ``PyJWKClientError`` / Pydantic ``ValidationError`` -> ``ValueError``
  mapping.

https://claude.ai/code/session_01LfuLSQf3rcbYG1QdgL92a5
@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants