← Back to Research

The pull request that owns your pipeline: CVE-2026-42298 in Postiz

The shape that matters. A CI workflow that builds a Docker image for incoming pull requests checked out the attacker's branch and ran the attacker's Dockerfile.dev, while holding a GITHUB_TOKEN with write-all permissions. CVE-2026-42298 (CVSS up to 10.0, CWE-94) is the result: any unauthenticated person who can open a pull request against Postiz gets arbitrary code execution inside CI and can exfiltrate a token that owns the repository. The build step became the breach.

Postiz is a popular open-source AI social-media scheduler. On May 8, 2026 its maintainers disclosed CVE-2026-42298, a textbook Pwn Request: a continuous-integration workflow that trusts code from a fork before that code has been reviewed. The fix landed the same day, in commit da44801, shipped as version 2.21.7. The bug is worth a careful read not because Postiz is unusually careless, but because the exact pattern that broke it is sitting in thousands of public and private repositories right now.

What actually broke

The vulnerable workflow was .github/workflows/pr-docker-build.yml, the “Build and Publish PR Docker Image” job. Its purpose was reasonable: when a contributor opens a pull request, build the proposed Docker image so reviewers can test it. The problem is the combination of three things that are each individually fine and catastrophic together:

Stitch those together and the attack is trivial. Fork Postiz. Edit Dockerfile.dev in your fork to add a RUN step that reads GITHUB_TOKEN from the build environment and exfiltrates it. Open a pull request back to the upstream repository. The CI runner dutifully checks out your branch and builds your Dockerfile, your RUN step executes with no review and no authentication, and your payload walks away with a token that can write to the project. No maintainer ever clicks anything. That is why the CNA scored it a perfect 10.0: remote, no privileges, no user interaction, and the scope crosses from the build sandbox into the whole repository.

Why “Pwn Request” is the right name

The name comes from the GitHub Actions failure mode where a workflow grants write-level trust to the contents of an untrusted pull request. The usual culprit is the pull_request_target trigger. Unlike the ordinary pull_request trigger, which runs in a read-only, secret-less context for fork PRs, pull_request_target runs in the context of the base repository: it has the repository's secrets and a writable token. That trigger exists for legitimate reasons (labeling PRs, posting review comments) where you need write access but only ever run trusted code from the base branch.

The bug is using that privileged trigger and then checking out the pull request head, so you are running untrusted code in a trusted context. The token and secrets that were supposed to stay on the maintainer's side of the wall are suddenly inside the attacker's build. Postiz's variant runs the attacker's Dockerfile, but the same class shows up as a malicious npm install postinstall script, a doctored test runner, a build plugin, or a Makefile target. The sink changes; the root cause does not.

Why this class is so persistent

CI/CD pipelines invert the usual trust model and almost nobody notices. In application code you spend enormous effort sanitizing input before it reaches a dangerous sink. In a build pipeline, the input is code, and the entire job of the pipeline is to execute it. The dangerous sink is the whole runner. So the only safe boundary is who is allowed to make that code run, and with what privileges. When a workflow lets an unauthenticated outsider trigger execution of their own code while a write-scoped token is in the environment, there is no sanitizer that helps. The control is privilege, not validation.

It persists because the insecure version is the convenient version. Building a contributor's image automatically on every PR is genuinely useful, and the privileged trigger is the path of least resistance to making secrets and a token available to that build. The secure version requires deliberately splitting the workflow into an untrusted phase (build and test the fork's code with no secrets and a read-only token) and a separate, gated trusted phase. That is more work, so the convenient pattern keeps getting copied from one repository to the next, token and all.

The defensible test

For your own estate the question is not “do we run Postiz” in isolation. It is two questions: is a vulnerable Postiz version deployed, and do any of our own repositories carry the same Pwn Request pattern. Both are answerable from artifacts you already have, without firing a single payload at production.

# 1. Is a vulnerable Postiz deployed? Confirm the version, do not guess.
#    Fixed in 2.21.7 (commit da44801). Anything earlier is vulnerable.
docker image ls --format '{{.Repository}}:{{.Tag}}' | grep -i postiz
docker inspect ghcr.io/gitroomhq/postiz-app:latest \
  --format '{{ index .Config.Labels "org.opencontainers.image.version" }}'

# 2. Do our own repos carry the Pwn Request pattern? Grep the workflow files.
#    FINDING = a privileged trigger that then checks out the PR head.
grep -rnE "pull_request_target" .github/workflows/
grep -rnE "ref:\s*\$\{\{\s*github\.event\.pull_request\.head" .github/workflows/
# A hit on BOTH in the same workflow = an untrusted checkout under a writable token.
# Cross-check the token: a job-level "permissions:" of read-only is a strong PASS.

The finding is the conjunction, not either half alone. A pull_request_target workflow that never checks out the PR head is fine. A workflow that checks out the PR head under the ordinary pull_request trigger has no token to steal. The exploitable state is the overlap: untrusted code executed while a privileged token is in scope.

The fix

How Celvex Sentry tests for this

Our continuous-monitoring suite carries two complementary checks here. The first is a version-and-deployment check for the dependency class: it resolves whether a vulnerable Postiz image is actually running and reachable, rather than alerting on the mere presence of the name. The second is a CI/CD posture check that parses workflow definitions for the Pwn Request shape: a privileged trigger that checks out untrusted pull-request code while a writable token is in scope. A read-only token, a gated trusted phase, or a build that never touches fork code is a PASS, not a noise finding. When the exploitable conjunction holds, we mint a Proof Capsule with the offending workflow lines, the token scope, the trigger, and the split-the-workflow fix attached, so you get the evidence and the remediation in one artifact.

Sources

Get your exposure check: full report in 4-24 hours

Real assessment on production-grade infrastructure. We prove what is exploitable and attach the fix. Paying customers get priority capacity.

Queue My Assessment