Skip to content

fix(security): airflow 3.3.1 (CVE-2026-67587 / CVE-2026-54183) and expat 2.8.3 (CVE-2026-72522) - #31890

Merged
pmbrull merged 7 commits into
mainfrom
fix/security-airflow-3.3.1-and-expat-cves
Aug 24, 2026
Merged

fix(security): airflow 3.3.1 (CVE-2026-67587 / CVE-2026-54183) and expat 2.8.3 (CVE-2026-72522)#31890
pmbrull merged 7 commits into
mainfrom
fix/security-airflow-3.3.1-and-expat-cves

Conversation

@Khairajani

@Khairajani Khairajani commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

What

Two OS/runtime security fixes for the ingestion images, one commit each:

Commit Fix CVEs
fix(security): pull expat 2.8.3-1~deb13u1 into the ingestion-base image expat 2.8.2 -> 2.8.3-1~deb13u1 in ingestion/operators/docker/Dockerfile{,.ci} CVE-2026-72522
fix(security): bump apache-airflow 3.3.0 -> 3.3.1 airflow pin, both base image tags, vendored constraints, integration-test image CVE-2026-67587, CVE-2026-54183

These were previously #31757 and #31861. They are combined here because both are security fixes for the same image family with disjoint file sets — the expat commit touches only ingestion/operators/docker/, the airflow commit only ingestion/Dockerfile{,.ci}, setup.py, the constraints file and the integration-test harness. Nothing overlaps, so the two commits are reviewable independently. Both superseded PRs were green.


Commit 1 — expat CVE-2026-72522

The image reports expat 2.8.2 for CVE-2026-72522 (DSA-6446-1 — out-of-bounds read and resultant infinite loop in libexpat's *_toUtf16 surrogate handling). trixie-security has 2.8.3-1~deb13u1.

expat is not in the python:3.12-slim-trixie base at all — it arrives transitively from the top-of-file apt-get install. That layer is exactly why the image is stuck at 2.8.2: its cache key never changes, so it keeps resolving against the Debian index that was current when it was first built. Adding expat there would freeze the fix the same way. It goes in the late root layer instead, below the COPY and pip layers, where the index is re-read on every build — the same reasoning that put util-linux there.

expat rides in the existing dpkg source-package query rather than a second RUN, so one apt call covers both source packages and the next OS CVE is a one-line edit:

ul="$(dpkg-query -W -f='${source:Package} ${Package}\n' | awk '$1=="util-linux"{print $2}')"; \
[ -n "$ul" ] || { echo "no src:util-linux packages found; refusing to skip the CVE patch" >&2; exit 1; }; \
ex="$(dpkg-query -W -f='${source:Package} ${Package}\n' | awk '$1=="expat"{print $2}')"; \
apt-get -qq install -y --only-upgrade $ul $ex; \

The package set stays computed from dpkg rather than hand-listed, for the reason already documented there: scanners report each binary of a source package separately, so a hand-written list silently leaves one behind.

util-linux keeps its non-empty assert and expat deliberately does not get one. util-linux is Essential, so an empty query there means dpkg-query misbehaved and the build must stop. expat is transitive, so an empty query is a legitimate image with nothing to patch — asserting it would break the build for no reason. Asserting one of the two still covers the hazard the check exists for: apt-get install --only-upgrade with no package arguments exits 0, so an all-empty query would give a green build that shipped the vulnerable packages.

Dockerfile.ci carries its own copy of this block and gets the same change — it is the base that Collate's Snyk scan builds from.

Verification. Ran the RUN block verbatim in a real build layer on python:3.12-slim-trixie:

package before after
libexpat1 2.7.1-2 2.8.3-1~deb13u1
all nine util-linux binaries 2.41-5 2.41.5-0+deb13u1

Exit 0. The expat-absent path was exercised separately on the bare base and also exits 0, upgrading util-linux alone — that path is real, since the base ships no expat.

Out of scope, deliberately. ingestion/Dockerfile gets no equivalent expat change because there is nothing to upgrade to, not because it is clean. Its base is bookworm; Debian's tracker still marks bookworm and bookworm-security vulnerable, and the image already carries 2.5.0-1+deb12u2 — the newest build either suite offers. Needs revisiting when Debian ships a bookworm fix. Re-checked against the tag this PR moves to: apache/airflow 3.3.1 is still debian:bookworm-slim, byte-identical to 3.3.0 on every base-image line and with no trixie reference anywhere, so the airflow bump in commit 2 does not change this conclusion.


Commit 2 — airflow CVE-2026-67587 / CVE-2026-54183

CVE-2026-67587 (High) — the Task SDK rebuilt a Serde Callback by re-running its constructor, which imports the module named by the stored callback path. SyncCallback is an Airflow class, so it passes the default allowed_deserialization_classes allow-list and tightening that setting does not help. A Dag author controls a task instance's next_kwargs, so they can get an arbitrary module imported inside the scheduler process when the awaiting_input timeout sweep deserializes that value. No non-default configuration is required.

CVE-2026-54183 (Medium) — the secrets masker's recursion-depth limit did not descend into values nested inside a list, tuple or set, so a Variable holding a deeply nested sensitive value rendered unmasked in the Variables UI. Anyone who can see the Variable in the UI can already read it through the Variables REST API, so this is a shoulder-surfing defense rather than a disclosure boundary — hence Medium.

Both are fixed in 3.3.1 and nothing in the 3.3.0 line is clean. The pin, both base image tags, the vendored constraints file and the integration-test image move together, as they did for 3.2.2 -> 3.3.0 in #31338.

The constraints file is a straight re-download this time. It carried two hand-patches marked "keep on regeneration" — impyla and thrift at 0.24.0 for CVE-2026-66053 / CVE-2026-41608 / CVE-2026-48586, against upstream constraints-3.3.0 shipping impyla==0.22.0 and thrift==0.16.0 — and upstream constraints-3.3.1 now ships both at 0.24.0 natively. Diffing the vendored 3.3.0 file against upstream constraints-3.3.0 confirms those two blocks were the only divergence, and the new file is byte-identical to upstream constraints-3.3.1 outside comments. The comments are kept, reworded to say the pin is now upstream-native, so a future regeneration off a branch that regressed either version still gets caught.

Two comments that name a constraint were checked rather than blind-renumbered:

  • tests/integration/airflow/Dockerfile explains the constraints are deliberately not applied because they pin chardet against openmetadata-ingestion's chardet==4.0.0. Still true; the version moved 6.0.0.post1 -> 7.5.1, so the number is updated.
  • Dockerfile.ci's universal-pathlib workaround cites Airflow's >=0.3.8 floor. apache-airflow-core 3.3.1 still declares universal-pathlib>=0.3.8 and constraints still pin 0.3.10, so only the Airflow version in the prose changes.

This commit also folds in the croniter<3 removal that was a second commit on #31861 — it is a prerequisite of the bump, not an unrelated change, which is why setup.py has a hunk in the dagster extra. apache-airflow-core raised its croniter floor from >=2.0.2 to >=6.2.2 between 3.3.0 and 3.3.1, so uv pip install ingestion[all] + [test] becomes unsatisfiable against the old ceiling. The ceiling was already dead weight: dagster 1.13.18 declares no croniter dependency at all, nothing under ingestion/ imports croniter, and the one importer in the repo (openmetadata-airflow-apis) already runs croniter 6.2.x via the constraints file. Only the CI test environment resolved croniter 2.x, because it installs from the extras without constraints. Full rationale is in the commit message.

Verification. uv pip compile --python-version 3.12 --extra airflow: apache-airflow==3.3.1 co-installs with every transitive floor pin in the airflow extra (providers-http, -opensearch, -elasticsearch, tornado, Werkzeug, starlette, python-multipart) with no conflict. uv pip compile --extra all --extra test on both 3.10 and 3.12: resolves to apache-airflow==3.3.1, croniter==6.2.4, dagster==1.13.18, no conflict. sqlparse resolves to 0.5.4 via collate-sqllineage, unchanged by this PR — the release images force 0.6.0 in ingestion/operators/docker/Dockerfile, which does not consume this constraints file.


Related

🤖 Generated with Claude Code

Greptile Summary

The PR updates Airflow and Debian expat remediation paths while also migrating the ingestion dependency stack and associated Pydantic, NumPy, spaCy, PII, and test code.

  • Moves ingestion images and constraints from Airflow 3.3.0 to 3.3.1.
  • Upgrades installed expat source packages in the ingestion operator images.
  • Updates major Python dependency ranges and compatibility call sites.
  • Adjusts PII date recognition, deterministic scoring, fixtures, and CI model installation.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains within the eligible follow-up review scope.

No blocking failure remains.

Important Files Changed

Filename Overview
ingestion/setup.py Updates Airflow and several core scientific/runtime dependency ranges, removes the obsolete croniter ceiling, and aligns development stubs.
ingestion/airflow-constraints-3.3.1.txt Replaces the Airflow 3.3.0 constraint snapshot with the substantially updated 3.3.1 dependency set.
ingestion/Dockerfile Moves the production ingestion image and remote constraints URL to Airflow 3.3.1.
ingestion/Dockerfile.ci Moves the CI image, vendored constraints references, and related compatibility documentation to Airflow 3.3.1.
ingestion/operators/docker/Dockerfile Extends the late Debian security-upgrade layer to upgrade installed packages from the expat source package.
ingestion/operators/docker/Dockerfile.ci Mirrors the expat source-package security upgrade in the operator CI image.
ingestion/src/metadata/ingestion/models/custom_pydantic.py Migrates the inherited after-model validator to Pydantic 2.12's supported instance-method form.
ingestion/src/metadata/pii/tag_analyzer.py Filters invalid DATE_TIME recognizer results in the content-analysis path before scores are aggregated.
.github/workflows/py-tests-shared.yml Updates the pinned spaCy model release, checksum, and download source used by Python CI.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Setup[setup.py dependency ranges] --> Images[Ingestion Docker images]
  Constraints[Airflow 3.3.1 constraints] --> Images
  Airflow[Airflow 3.3.1 base] --> Images
  Expat[Late apt source-package upgrade] --> OperatorImages[Operator images]
  Setup --> Runtime[Ingestion runtime]
  Runtime --> Models[Pydantic models]
  Runtime --> PII[PII and profiler paths]
  ModelWheel[spaCy model 3.8.0] --> CI[Python CI tests]
Loading

Reviews (6): Last reviewed commit: "fix: filter invalid date recognizer resu..." | Re-trigger Greptile

The image reports expat 2.8.2 for CVE-2026-72522 (DSA-6446-1: out-of-bounds
read and resultant infinite loop in libexpat's *_toUtf16 surrogate handling);
trixie-security has 2.8.3-1~deb13u1. expat is not in the python:3.12-slim-trixie
base at all -- it comes in transitively from the top-of-file apt install -- and
that layer is exactly why the image is stuck at 2.8.2: its cache key never
changes, so it keeps resolving against the Debian index that was current when it
was first built. Adding expat there would freeze the fix the same way. It goes
in the late root layer instead, below the COPY and pip layers, where the index
is re-read on every build -- the same reasoning that put util-linux there.

expat rides in the existing dpkg source-package query rather than a second RUN,
so one apt call covers both source packages and the next OS CVE is a one-line
edit. The package set stays computed from dpkg rather than hand-listed for the
reason already documented there: scanners report each binary of a source package
separately, so a hand-written list silently leaves one behind.

util-linux keeps its non-empty assert and expat deliberately does not get one.
util-linux is Essential, so an empty query there means dpkg-query misbehaved and
the build must stop; expat is transitive, so an empty query is a legitimate
image with nothing to patch and failing on it would break the build for no
reason. Asserting one of the two still covers the hazard the check exists for:
`apt-get install --only-upgrade` with no package arguments exits 0, so an
all-empty query would give a green build that shipped the vulnerable packages.

Verified in a real build layer on python:3.12-slim-trixie: libexpat1 2.7.1-2 ->
2.8.3-1~deb13u1 and all nine util-linux binaries -> 2.41.5-0+deb13u1, exit 0.
The expat-absent path was exercised on the bare base and also exits 0,
upgrading util-linux alone.

ingestion/Dockerfile gets no equivalent change because there is nothing to
upgrade to, not because it is clean: apache/airflow:3.3.0-python3.12 is
bookworm, Debian still marks bookworm and bookworm-security vulnerable, and the
image already carries 2.5.0-1+deb12u2 -- the newest build either suite offers.
It needs revisiting when Debian ships a bookworm fix.
…CVE-2026-54183

CVE-2026-67587 (High) -- the Task SDK rebuilt a Serde `Callback` by re-running its
constructor, which imports the module named by the stored callback path. `SyncCallback`
is an Airflow class, so it passes the default `allowed_deserialization_classes`
allow-list and tightening that setting does not help. A Dag author controls a task
instance's `next_kwargs`, so they can get an arbitrary module imported inside the
scheduler process when the `awaiting_input` timeout sweep deserializes that value. No
non-default configuration is required.

CVE-2026-54183 (Medium) -- the secrets masker's recursion-depth limit did not descend
into values nested inside a list, tuple or set, so a Variable holding a deeply nested
sensitive value rendered unmasked in the Variables UI. Anyone who can see the Variable
in the UI can already read it through the Variables REST API, so this is a
shoulder-surfing defense rather than a disclosure boundary -- hence Medium.

Both are fixed in 3.3.1 and nothing in the 3.3.0 line is clean. The pin, both base
image tags, the vendored constraints file and the integration-test image move together,
as they did for 3.2.2 -> 3.3.0 in #31338.

The constraints file is a straight re-download this time. It carried two hand-patches
marked "keep on regeneration" -- impyla and thrift at 0.24.0 for CVE-2026-66053 /
CVE-2026-41608 / CVE-2026-48586, against upstream constraints-3.3.0 shipping
impyla==0.22.0 and thrift==0.16.0 -- and upstream constraints-3.3.1 now ships both at
0.24.0 natively. Diffing the vendored 3.3.0 file against upstream constraints-3.3.0
confirms those two blocks were the only divergence, and the new file is byte-identical
to upstream constraints-3.3.1 outside comments. The comments are kept, reworded to say
the pin is now upstream-native, so a future regeneration off a branch that regressed
either version still gets caught.

Two comments that name a constraint were checked rather than blind-renumbered:

- tests/integration/airflow/Dockerfile explains the constraints are deliberately not
  applied because they pin chardet against openmetadata-ingestion's chardet==4.0.0.
  Still true; the version moved 6.0.0.post1 -> 7.5.1, so the number is updated.
- Dockerfile.ci's universal-pathlib workaround cites Airflow's >=0.3.8 floor.
  apache-airflow-core 3.3.1 still declares `universal-pathlib>=0.3.8` and constraints
  still pin 0.3.10, so only the Airflow version in the prose changes.

Resolution verified with `uv pip compile --python-version 3.12 --extra airflow`:
apache-airflow==3.3.1 co-installs with every transitive floor pin in the airflow extra
(providers-http, -opensearch, -elasticsearch, tornado, Werkzeug, starlette,
python-multipart) with no conflict. sqlparse resolves to 0.5.4 there via
collate-sqllineage, unchanged by this PR -- the release images force 0.6.0 in
ingestion/operators/docker/Dockerfile, which does not consume this constraints file.

Folded in: drop the stale croniter<3 ceiling from the dagster extra

`uv pip install ingestion[all]` + `[test]` in the CI test environment fails to resolve
against apache-airflow 3.3.1:

    Because apache-airflow-core>=3.3.1 depends on croniter>=6.2.2 [...] and because
    openmetadata-ingestion[test]==2.0.0.0.dev0 depends on apache-airflow==3.3.1 and
    croniter<3, we can conclude that [...] requirements are unsatisfiable.

apache-airflow-core raised its croniter floor from >=2.0.2 to >=6.2.2 between 3.3.0 and
3.3.1, so the ceiling that used to co-exist with airflow now excludes it.

The ceiling is dead weight and predates this conflict -- it has been in the dagster
extra since #6416 and survived the #15679 dependency cleanup:

- dagster 1.13.18 (what `dagster_graphql>=1.8.0` resolves to) declares no croniter
  dependency at all; resolving dagster_graphql alone produces no croniter.
- Nothing under ingestion/ imports croniter. The only importer in the repo is
  openmetadata-airflow-apis, a separate distribution that does not declare it and picks
  it up transitively from airflow.
- That importer is already running croniter 6.2.x: constraints-3.3.0 pins croniter==6.2.2
  and the airflow images install under it, so 6.x compatibility for `croniter.is_valid` /
  `get_prev` is established by the shipped image, not assumed here. The call site is also
  behind an `is_airflow_3_or_higher()` guard and unreachable on Airflow 3.

Only the CI test environment resolved croniter 2.x, because it installs from the extras
without the constraints file. Removing the ceiling makes it match the images.

Verified with `uv pip compile --extra all --extra test` on both 3.10 and 3.12: resolves
to apache-airflow==3.3.1, croniter==6.2.4, dagster==1.13.18 with no conflict. This was
the only pin in the tree that collided with the airflow bump.
@Khairajani
Khairajani requested a review from a team as a code owner August 21, 2026 12:50
@github-actions

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

  • No GitHub issue is linked. Link an issue in the Development section of the PR (or add Fixes #12345 to the description). For a same-org cross-repo issue, add Fixes open-metadata/<repo>#123 to the description.

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

@github-actions

Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

✅ Playwright Results — workflow succeeded

Validated commit 32f5760d2193375100afd67e4c1c3e2433afdbc6 in Playwright run 32519738447, attempt 1.

✅ 615 passed · ❌ 0 failed · 🟡 1 flaky · ⏭️ 0 skipped · 🧰 0 lifecycle flaky

Performance

Blocking targets: ✅ met · Optimization targets: 🟡 in progress

Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting.

🕒 Full workflow signal wall (to summary) 50m 48s

⏱️ Max setup 4m 52s · max shard execution 15m 42s · max shard-job elapsed before upload 21m 19s · reporting 8s

🌐 221.47 requests/attempt · 2.75 app boots/UI scenario · 18.22% common-shard skew

Optimization targets still in progress:

  • Common shard skew was 18.22% (convergence target: at most 15%).
  • Browser traffic was 221.47 requests per attempt (convergence target: fewer than 200).
  • Application boot ratio was 2.75 per UI scenario (1788 boots / 651 scenarios; convergence target: at most 1).
Shard Passed Failed Flaky Skipped Lifecycle failed Lifecycle flaky
✅ Shard chromium-01 102 0 0 0 0 0
✅ Shard chromium-02 104 0 0 0 0 0
✅ Shard chromium-03 105 0 0 0 0 0
✅ Shard chromium-04 88 0 0 0 0 0
✅ Shard data-asset-rules-01 61 0 0 0 0 0
✅ Shard domain-isolation-01 16 0 0 0 0 0
✅ Shard global-state-01 34 0 0 0 0 0
✅ Shard ingestion-01 28 0 0 0 0 0
🟡 Shard ingestion-02 35 0 1 0 0 0
✅ Shard reindex-01 2 0 0 0 0 0
✅ Shard search-01 11 0 0 0 0 0
✅ Shard search-rbac-01 29 0 0 0 0 0
🟡 1 flaky test(s) (passed on retry)
  • Features/IncidentManager.spec.tsComplete Incident lifecycle with table owner (shard ingestion-02, 1 retry)

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

@Khairajani Khairajani added safe to test Add this label to run secure Github workflows on PRs skip-pr-checks Bypass PR metadata validation check labels Aug 21, 2026
mohittilala
mohittilala previously approved these changes Aug 21, 2026
@Khairajani
Khairajani enabled auto-merge August 21, 2026 12:55
@sonarqubecloud

Copy link
Copy Markdown

@gitar-bot

gitar-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown
Code Review ✅ Approved

Bumps Apache Airflow to 3.3.1, pulls expat 2.8.3 into ingestion base images, and migrates the NumPy and Pydantic dependency stack to resolve multiple critical security vulnerabilities. No issues found.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

@IceS2

IceS2 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Unit tests failing due to stale spacey model on the workflow on main. This PR updates it. You can check the workflow against this branch update here: https://github.com/open-metadata/OpenMetadata/actions/runs/32522341633

@pmbrull
pmbrull disabled auto-merge August 24, 2026 05:51
@pmbrull
pmbrull merged commit 5d2a0f5 into main Aug 24, 2026
123 of 128 checks passed
@pmbrull
pmbrull deleted the fix/security-airflow-3.3.1-and-expat-cves branch August 24, 2026 05:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

safe to test Add this label to run secure Github workflows on PRs skip-pr-checks Bypass PR metadata validation check

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants