Hi Meshery security team,
Reporting a pull_request_target vulnerability in meshery/meshery that allows any external fork PR to execute code in the privileged base repository context, with contents: write and pull-requests: write on the workflow token.
.github/workflows/build-and-preview-docs.yml (job build-and-deploy-preview), as of commit 2026-05-08T00:49:02Z on master.
The workflow triggers on pull_request_target for changes to docs/**, mesheryctl/doc/**, or the workflow file itself. It checks out the PR head (${{ github.event.pull_request.head.repo.full_name }}@${{ github.event.pull_request.head.sha }}) with persist-credentials: false. That setting prevents the token from being written to .git/config, but it does not prevent later steps from being attacked through the workspace. npm ci in the attacker-controlled docs/ directory then executes lifecycle scripts from the fork's docs/package.json.
I reproduced the issue safely using a private mirror of the workflow under accounts I control. A malicious preinstall script in docs/package.json from a fork PR executed within roughly 3 seconds on a GitHub-hosted Linux runner. The runner environment confirmed GITHUB_REPOSITORY pointed to the base repo under pull_request_target. The mirror and fork were deleted immediately after capture.
The canary script printed only the names of secret-related environment variables and marker strings. No secret values were captured. No issue, comment, PR, or workflow run was created against meshery/meshery itself.
The repository setting "Require approval for fork pull request workflows" is not blocking external contributors from this workflow. Across the 20 most recent runs of build-and-preview-docs.yml from 7 distinct external forks (Chaithanya5gif, CodexRaunak, YASHMAHAKAL, Yash-Raj-5424, banana-three-join, guan404ming, shteypandey28-hue), zero runs landed at conclusion: action_required. Each new contributor's first run completed without manual approval. If the gate were enabled, those first runs would have stayed queued.
The gh-pages branch has no branch protection rule and no repository ruleset visible to outside callers (GET /branches/gh-pages/protection returns 404; GET /rules/branches/gh-pages returns an empty list).
The token is not directly available in npm lifecycle scripts (GitHub injects secrets.GITHUB_TOKEN only into steps that reference it), so a preinstall script cannot read the token from its own environment. A second mirror PR validated three reachable chains end to end on a real GitHub-hosted runner. Each chain emitted a distinct marker; all three landed in the run log.
preinstall writes a malicious Makefile to $GITHUB_WORKSPACE root. The next step (make docs-build-production) reads it and runs the attacker target. Markers captured: CHAIN1_MAKEFILE_OVERRIDE_EXECUTED and CHAIN1_REPO_CTX=<base-repo>./home/runner/work/_actions/<owner>/<repo>/<ref>/ before the job's first user step. preinstall overwrites the cached entry point. The mirror confirmed this against actions/github-script@v7 by rewriting /home/runner/work/_actions/actions/github-script/v7/dist/index.js; when the workflow's later uses: step invoked the action, the runner executed the substituted script (CHAIN2_ACTION_POISONED_AND_EXECUTED). In meshery's workflow, rossjrw/pr-previ...@v1.6.3 is the natural target because it receives the workflow GITHUB_TOKEN directly.$GITHUB_ENV PATH override. preinstall writes PATH=/tmp/atk-shim:<original PATH> to $GITHUB_ENV and plants a git shim at that path. Every subsequent git invocation routes through the shim. When the Chain 3 step ran git remote set-url origin "https://x-access-token:${GH_TOKEN}@...", the shim recorded CHAIN3_TOKEN_LEAKED_LEN=40 and CHAIN3_TOKEN_PREFIX=ghs_SU (the ghs_ prefix is the GitHub Actions installation token format, 40 characters, carrying the workflow's declared scopes). The mirror's chain then completed an actual git push to the victim repo using the captured token, creating a chain3-canary branch as direct proof of contents: write flowing from fork PR to base-repo modification. The Prune old PR previews step in build-and-preview-docs.yml invokes exactly this git push pattern.Meshery is currently exposed. The captured token is the same identity already pushing to gh-pages in production: the Prune old PR previews step commits to gh-pages as github-actions[bot] on every run (recent examples: commits 6c95655c and 4c3f9148, both authored by github-actions[bot] with the message "Prune old PR previews"). pr-preview-action already mirrors fork-PR-author commits onto gh-pages (recent examples: commit 4b3eed8b authored by external contributor shteypandey28-hue for PR 19170, b5cdbd0a authored by YASHMAHAKAL for PR 19168). The vulnerability lets the attacker control the content of those deploys, not just trigger them.
gh-pages is served at docs.meshery.io, a domain trusted by the Meshery user base. Persistent XSS, modified installation instructions, and modified references to downloadable assets and Helm chart sources are reachable from this primitive. Any visitor of the docs runs attacker JavaScript on a meshery.io subdomain.
docs.meshery.io content.contents: write scope reaches without a blocking ruleset. master is reported as protected: true by GET /branches/master, but repository.branchProtectionRules and repository.rulesets both return empty via GraphQL to outside callers, and bypass-actor lists are not readable without organization admin scope. The only way to definitively verify whether the workflow token can or cannot push to master from outside is a live test against meshery/meshery itself, which I deliberately did not attempt: a successful push would be a real modification of your repository, and a failed push would still leave a workflow run and rejected push artifact in your audit log. Please confirm internally whether github-actions[bot] is on the bypass list of any ruleset that protects master, release branches, or any branch consumed by your build or release pipelines.github-actions[bot] under pull-requests: write.GITHUB_TOKEN exfiltration and use during the run, with the workflow's declared scopes for the lifetime of the runner.Layered application is preferable.
pull_request_target only for safe operations, and move the deployment to a workflow_run-triggered job that consumes a trusted artifact built from the safe context.permissions: block to read-all and elevate only inside specific jobs that operate on already-built artifacts rather than fork source.if: github.event.pull_request.head.repo.full_name == github.repository.npm ci --ignore-scripts so lifecycle scripts in fork-controlled package.json cannot execute.actions/checkout, actions/setup-node, actions/github-script, and rossjrw/pr-preview-action by full commit SHA rather than tag.Please confirm receipt and let me know your preferred timeline for remediation and coordinated disclosure. I am happy to validate the fix privately and keep this report embargoed until you are ready.
Thank you for maintaining Meshery.
Devyn St. Ours
Begin forwarded message:
--
Visit and engage with the Meshery community in the forum at http://discuss.meshery.io or in Slack at https://slack.meshery.io.
---
You received this message because you are subscribed to the Google Groups "Meshery Maintainers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to maintainers...@meshery.io.
To view this discussion visit https://groups.google.com/a/meshery.io/d/msgid/maintainers/CA%2Brc-z00ddNnszmZhqAHpzohXC_VAKJb%3D-Lgq%2BVCYsJgFpy%2BGA%40mail.gmail.com.