Skip to content

fix(security): validate GitLab deploy artifacts - #121

Open
phantomii wants to merge 1 commit into
masterfrom
codex/fix-vulnerability-in-deployment-script
Open

fix(security): validate GitLab deploy artifacts#121
phantomii wants to merge 1 commit into
masterfrom
codex/fix-vulnerability-in-deployment-script

Conversation

@phantomii

Copy link
Copy Markdown
Member

Motivation

  • The existing deploy script selected the first successful build-web job matching a textual ref, which allowed tag or untrusted-pipeline artifacts to be mistaken for protected-branch builds and deployed to production.
  • This created a trust-boundary issue where attacker-controlled artifacts could be deployed by creating pipelines/tags with the same ref name.

Description

  • Require the configured ref to be a protected GitLab branch by resolving repository/branches/<ref> and reading the branch protected flag via _protected_branch_head_sha and fetch its HEAD SHA.
  • Filter candidate jobs in _latest_job_id to non-tag build-web jobs whose commit.id equals the protected branch HEAD SHA and whose pipeline source is in the trusted set {'push','schedule','web'}.
  • Add _pipeline_source helper to query pipelines/<id> for pipeline source.
  • Add unit tests (scripts/test_deploy_artifact.py) that cover: rejecting tag artifacts, rejecting unprotected refs, and rejecting pipelines from untrusted sources.

Testing

  • Ran the new unit tests with python3 -m unittest scripts/test_deploy_artifact.py, which passed.
  • Verified Python syntax/bytecode with python3 -m py_compile scripts/deploy-artifact.py scripts/test_deploy_artifact.py, which succeeded.
  • Ran static checks (git diff --check) to ensure no whitespace/errors were introduced, which reported no issues.

Codex Task

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +44 to +130
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",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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",
            )

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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"}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant