This guide documents the workflow for resolving CVEs in Python packages within the OpenDataHub Notebooks images.
Acknowledgment: This workflow was contributed by Adriana Theodorakopoulou.
Python CVEs in notebook images can come from:
- Direct dependencies: Packages explicitly listed in
pyproject.toml - Transitive dependencies: Packages pulled in by direct dependencies
The resolution strategy differs based on which type is affected.
To prevent CVEs from returning through transitive dependencies, we maintain a centralized constraints file:
dependencies/cve-constraints.txt
This file is automatically applied during lock file generation via uv pip compile --constraints. It ensures that even packages not explicitly in pyproject.toml (transitive dependencies) never go below the fixed version for CVEs we've resolved.
-
Constraints file format (requirements.txt style):
# CVE-ID: Description # Reference: https://... package>=fixed_version -
Automatic application: The
pylocks_generator.shscript applies these constraints to all lock file generations. -
Override for conflicts: Some packages (like odh-elyra's appengine-python-standard) have conflicting version requirements. For these, use
override-dependenciesin the specific image'spyproject.toml.
-
Add the constraint to
dependencies/cve-constraints.txt:# RHAIENG-XXXX: CVE-YYYY-ZZZZZ package_name vulnerability description # Upstream: https://github.com/... package_name>=fixed_version -
Regenerate all lock files:
make refresh-lock-files # or bash scripts/pylocks_generator.sh public-index -
If resolution fails due to conflicts, add
override-dependenciesto the affected image'spyproject.toml.
Example: RHAIENG-2448 - Tornado quadratic DoS repeated header
- Open the Jira ticket and identify the package name (e.g., "tornado")
- Check which images are affected (often all images from minimal to trustyai, tensorflow, pytorch, etc.)
- Open one of the linked Jiras from ProdSec to see the summary
From the CVE summary, identify:
- Affected versions: e.g., "version 6.5.2 and below"
- Fixed version: e.g., "fixed in version 6.5.3"
# Search in pyproject.toml files
grep -r "tornado" --include="pyproject.toml" .
# Search in pylock.toml files
grep -r "tornado" --include="pylock.toml" .Determine if it's a:
- Direct dependency: Found in
pyproject.toml - Transitive dependency: Only found in
pylock.toml
For transitive dependencies, find which direct dependency pulls it in:
# Using uv (preferred)
uv tree | grep -A5 -B5 tornado
# Or check the package's dependents
uv tree --invert tornadoExample: Tornado is typically pulled in by jupyter-server.
Before attempting to fix the CVE, check that the fixed version is actually available on the RH index. A version may exist on PyPI but not on the RH index — this is a common blocker for downstream (RHOAI) images.
# Check which versions are on the production RH index
curl -sL "https://packages.redhat.com/api/pypi/public-rhai/rhoai/3.4/cpu-ubi9/simple/<package>/?format=json" \
| python3 -c "
import json,sys
data = json.load(sys.stdin)
versions = sorted({f['filename'].split('-')[1] for f in data.get('files',[])})
print('\n'.join(versions))
"There are three layers that can block a CVE fix from resolving:
-
RH index: The fixed version may not have been built by AIPCC yet. If missing, request it via the AIPCC dashboard.
-
Builder constraints: The
wheels/builderproject may pin an older version incollections/torch-*/constraints.txt. For example,onnx==1.20.0was pinned with# AIPCC-7623 - Constraining onnx while we carry patches, preventing 1.21.0 from appearing on the index even after the onboarding pipeline ran. Check with:glab api --hostname gitlab.com "projects/58339326/search?scope=blobs&search=<package>%3D%3D" -
Upstream SDK exact pins: Packages like
codeflare-sdkmay exact-pin the vulnerable version (e.g.,cryptography==46.0.6,ray[data]==2.53.0). These conflicts surface duringmake refresh-lock-filesas "unsatisfiable" errors. Coordinate with the owning team per the component-team-owned packages procedure.
If the fixed version is not on the RH index, do not proceed to Step 5 — the lock refresh will fail. Instead, request the build and revisit once it is available.
- Check the latest version on pypi.org
- Check the upstream package's
pyproject.tomlto see their version constraints - Update the version in your
pyproject.toml:"jupyter-server~=2.17.0", # Updated for tornado CVE fix
If the direct dependency can't be upgraded but the transitive package version is flexible:
-
Add to
dependencies/cve-constraints.txt:# RHAIENG-2448: CVE-XXXX-YYYY tornado quadratic DoS tornado>=6.5.3 -
Regenerate lock files - the constraint will be applied automatically.
If there are version conflicts that prevent constraint-based resolution:
[tool.uv]
override-dependencies = [
# RHAIENG-2448: CVE-XXXX-YYYY tornado - override needed due to version conflict
"tornado>=6.5.3",
]Note: Override dependencies force the specified version, potentially breaking packages that genuinely can't work with it. Use sparingly.
# Regenerate lock files
make refresh-lock-files
# Build the affected image(s)
make jupyter-datascience-ubi9-python-3.12- Go to Konflux and find the Tekton build pipeline for your image
- Open the clair-scan task logs
- Search for the CVE number (e.g.,
CVE-2024-XXXXX) - If the CVE is not found in the logs, the fix is validated
- Go to the "push build notebooks" GitHub Action
- Check the "Vulnerability Report by Trivy" section
- Search for the CVE number
- If the CVE is not present after the fix, validation is successful
Note: Trivy is more sensitive than Konflux's Clair scan. A CVE may appear in Trivy but not in Clair. Always validate against the downstream Konflux scans for production images.
- Identify: urllib3 decompression vulnerability, affects all images
- Fixed version: urllib3 >= 2.6.0
- Type: Transitive dependency (pulled in by many packages)
- Conflict: odh-elyra depends on appengine-python-standard which requires urllib3<2
Solution:
-
Add to
dependencies/cve-constraints.txtfor general protection:# RHAIENG-2458: CVE-2025-66418 urllib3 decompression vulnerability urllib3>=2.7.0 -
Add override to jupyter images with odh-elyra (due to conflict):
override-dependencies = [ # RHAIENG-2458: CVE-2026-44431 urllib3 - override (also CVE-2025-66418) needed because odh-elyra pulls in # appengine-python-standard which has an obnoxious urllib3<2 constraint "urllib3>=2.7.0", ]
-
Always add to centralized constraints first - This prevents CVEs from returning through any dependency path.
-
Use override-dependencies sparingly - Only when there's a genuine conflict that constraints can't resolve.
-
Document the CVE - Include RHAIENG ticket, CVE ID, and explanation in comments.
-
Validate in both Trivy and Clair - Trivy may catch issues Clair misses.
-
Consider upstream fixes - If a direct dependency has a newer version that fixes the transitive CVE, prefer upgrading the direct dependency.
dependencies/cve-constraints.txt- Centralized CVE constraintsscripts/pylocks_generator.sh- Lock file generator (applies constraints)pyproject.toml- Direct dependencies and override-dependenciespylock.toml/uv.lock.d/- Generated lock files
# Regenerate all lock files
make refresh-lock-files
# Regenerate lock files for specific directory
bash scripts/pylocks_generator.sh public-index jupyter/datascience/ubi9-python-3.12
# Check dependency tree
uv tree
# Find what depends on a package
uv tree --invert package-name
# Search for package in repository
grep -r "package-name" --include="*.toml" .