fix(security): validate GitLab deploy artifacts - #121
Conversation
There was a problem hiding this comment.
Code Review
This pull request enhances the artifact deployment script by ensuring that only jobs from trusted pipeline sources on protected branches matching the branch HEAD SHA are selected. It introduces helper functions to verify the protected branch HEAD SHA and the pipeline source, and updates the job selection logic accordingly. A review comment suggests updating the unit tests to align with the new pipeline-based implementation of _latest_job_id, providing a code block for the test cases.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| def test_skips_tag_artifacts_and_selects_protected_branch_head_job(self): | ||
| session = FakeSession({ | ||
| "/repository/branches/master": { | ||
| "protected": True, | ||
| "commit": {"id": "branch-head-sha"}, | ||
| }, | ||
| "/jobs": [ | ||
| { | ||
| "id": 9001, | ||
| "name": "build-web", | ||
| "ref": "master", | ||
| "tag": True, | ||
| "commit": {"id": "attacker-tag-sha"}, | ||
| "pipeline": {"id": 91}, | ||
| }, | ||
| { | ||
| "id": 1000, | ||
| "name": "build-web", | ||
| "ref": "master", | ||
| "tag": False, | ||
| "commit": {"id": "branch-head-sha"}, | ||
| "pipeline": {"id": 10}, | ||
| }, | ||
| ], | ||
| "/pipelines/10": {"source": "push"}, | ||
| }) | ||
|
|
||
| job_id = deploy_artifact._latest_job_id( | ||
| session, | ||
| "https://gitlab.example", | ||
| "group/project", | ||
| "master", | ||
| ) | ||
|
|
||
| self.assertEqual(job_id, 1000) | ||
| requested_urls = [url for url, _kwargs in session.calls] | ||
| self.assertIn( | ||
| "https://gitlab.example/api/v4/projects/group%2Fproject/repository/branches/master", | ||
| requested_urls, | ||
| ) | ||
| self.assertIn( | ||
| "https://gitlab.example/api/v4/projects/group%2Fproject/pipelines/10", | ||
| requested_urls, | ||
| ) | ||
|
|
||
| def test_rejects_unprotected_branch(self): | ||
| session = FakeSession({ | ||
| "/repository/branches/master": { | ||
| "protected": False, | ||
| "commit": {"id": "branch-head-sha"}, | ||
| }, | ||
| }) | ||
|
|
||
| with self.assertRaisesRegex(RuntimeError, "not a protected GitLab branch"): | ||
| deploy_artifact._latest_job_id( | ||
| session, | ||
| "https://gitlab.example", | ||
| "group/project", | ||
| "master", | ||
| ) | ||
|
|
||
| def test_rejects_untrusted_pipeline_source(self): | ||
| session = FakeSession({ | ||
| "/repository/branches/master": { | ||
| "protected": True, | ||
| "commit": {"id": "branch-head-sha"}, | ||
| }, | ||
| "/jobs": [ | ||
| { | ||
| "id": 1000, | ||
| "name": "build-web", | ||
| "ref": "master", | ||
| "tag": False, | ||
| "commit": {"id": "branch-head-sha"}, | ||
| "pipeline": {"id": 10}, | ||
| }, | ||
| ], | ||
| "/pipelines/10": {"source": "trigger"}, | ||
| }) | ||
|
|
||
| with self.assertRaisesRegex(RuntimeError, "No trusted successful"): | ||
| deploy_artifact._latest_job_id( | ||
| session, | ||
| "https://gitlab.example", | ||
| "group/project", | ||
| "master", | ||
| ) |
There was a problem hiding this comment.
Update the unit tests to match the new pipeline-based implementation of _latest_job_id.
def test_skips_tag_artifacts_and_selects_protected_branch_head_job(self):
session = FakeSession({
"/repository/branches/master": {
"protected": True,
"commit": {"id": "branch-head-sha"},
},
"/pipelines": [
{
"id": 10,
"source": "push",
"tag": False,
}
],
"/pipelines/10/jobs": [
{
"id": 1000,
"name": "build-web",
"status": "success",
"created_at": "2023-01-01T00:00:00Z",
}
],
})
job_id = deploy_artifact._latest_job_id(
session,
"https://gitlab.example",
"group/project",
"master",
)
self.assertEqual(job_id, 1000)
requested_urls = [url for url, _kwargs in session.calls]
self.assertIn(
"https://gitlab.example/api/v4/projects/group%2Fproject/repository/branches/master",
requested_urls,
)
self.assertIn(
"https://gitlab.example/api/v4/projects/group%2Fproject/pipelines",
requested_urls,
)
self.assertIn(
"https://gitlab.example/api/v4/projects/group%2Fproject/pipelines/10/jobs",
requested_urls,
)
def test_rejects_unprotected_branch(self):
session = FakeSession({
"/repository/branches/master": {
"protected": False,
"commit": {"id": "branch-head-sha"},
},
})
with self.assertRaisesRegex(RuntimeError, "not a protected GitLab branch"):
deploy_artifact._latest_job_id(
session,
"https://gitlab.example",
"group/project",
"master",
)
def test_rejects_untrusted_pipeline_source(self):
session = FakeSession({
"/repository/branches/master": {
"protected": True,
"commit": {"id": "branch-head-sha"},
},
"/pipelines": [
{
"id": 10,
"source": "trigger",
"tag": False,
}
],
})
with self.assertRaisesRegex(RuntimeError, "No trusted successful"):
deploy_artifact._latest_job_id(
session,
"https://gitlab.example",
"group/project",
"master",
)There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f18d243be0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # GitLab API | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| TRUSTED_PIPELINE_SOURCES = {"push", "schedule", "web"} |
There was a problem hiding this comment.
Exclude manual web pipelines from trusted artifacts
When a user who can run a protected-branch manual pipeline starts build-web, GitLab marks the pipeline source as web and allows user-supplied pipeline variables; this repo's build-web job writes $WEB_ENVS into packages/web/.env.production before building (.gitlab-ci.yml:57-62). Because web is whitelisted here, the deploy script can select and deploy an artifact for the protected branch HEAD that was built with ad-hoc manual variables rather than normal protected-branch automation, so the new artifact validation still permits config injection through manual pipeline runs.
Useful? React with 👍 / 👎.
Motivation
build-webjob matching a textual ref, which allowed tag or untrusted-pipeline artifacts to be mistaken for protected-branch builds and deployed to production.Description
repository/branches/<ref>and reading the branchprotectedflag via_protected_branch_head_shaand fetch its HEAD SHA._latest_job_idto non-tagbuild-webjobs whosecommit.idequals the protected branch HEAD SHA and whose pipelinesourceis in the trusted set{'push','schedule','web'}._pipeline_sourcehelper to querypipelines/<id>for pipelinesource.scripts/test_deploy_artifact.py) that cover: rejecting tag artifacts, rejecting unprotected refs, and rejecting pipelines from untrusted sources.Testing
python3 -m unittest scripts/test_deploy_artifact.py, which passed.python3 -m py_compile scripts/deploy-artifact.py scripts/test_deploy_artifact.py, which succeeded.git diff --check) to ensure no whitespace/errors were introduced, which reported no issues.Codex Task