Back to Intelligence

GitLab Unauthenticated Code Execution Under Active Attack — 1,000+ Internet-Exposed Servers Still Unpatched: Detection & Remediation Guide

SA
Security Arsenal Team
November 2, 2026
10 min read

More than 1,000 internet-exposed, self-managed GitLab servers remain unpatched against a maximum-severity unauthenticated code execution vulnerability — and attackers are actively exploiting it right now. This is not a theoretical exposure count from a passive scan; this is an ongoing exploitation campaign against instances that have had ample warning.

GitLab sits at the center of the software supply chain for most organizations that run it: source code, CI/CD pipelines, secrets, runner credentials, and deployment keys all live on that box. An unauthenticated remote code execution flaw on an internet-facing GitLab instance is effectively a skeleton key into your development infrastructure — and from there, into production. If you operate a self-managed GitLab Community Edition (CE) or Enterprise Edition (EE) instance and you have not patched to the latest security release, treat this as an incident, not a patch ticket.

Technical Analysis

What is affected

  • Product: Self-managed GitLab CE and EE instances (Omnibus, source, and Docker deployments)
  • Exposure: Internet-facing instances reachable on TCP 80/443 without upstream access controls
  • Cloud-hosted GitLab.com instances are operated by GitLab and are not the population at risk here — the vulnerable population is customer-managed servers

Severity and exploitation requirements

The vulnerability carries a maximum severity rating (CVSS 10.0-class) because it requires:

  • No authentication — the attacker needs no credentials, no account, and no valid session
  • No user interaction — exploitation is driven entirely by the attacker
  • Network access only — a single crafted request sequence to an exposed instance is sufficient to achieve code execution in the context of the GitLab service account

Code execution on a GitLab server typically lands as the git user (Omnibus installs) or the web service account. From there, attackers gain read access to every repository, the CI/CD variable store (which frequently contains cloud keys, deploy tokens, and registry credentials), runner registration tokens, and the PostgreSQL backend containing user data and OAuth/application secrets.

Exploitation status

  • Confirmed active exploitation in the wild. Reporting indicates over 1,000 servers remain exposed and attacks are ongoing — this is an operational campaign, not PoC noise.
  • The low barrier to entry (unauthenticated, network-reachable) means exploitation scales trivially with internet-wide scanning. Any instance enumerated by Shodan/Censys-style reconnaissance should be assumed targeted.

What post-exploitation looks like

From IR engagements involving compromised GitLab instances, the common attack chain after initial code execution includes:

  1. Webshell or reverse-shell staging — the GitLab web/worker processes (puma, gitlab-workhorse) spawn a shell (sh, bash, python) to establish interactive access
  2. Credential harvesting — dumping CI/CD variables, gitlab-secrets.json, database credentials, and runner tokens
  3. Persistence — cron entries, systemd units, SSH authorized_keys appended to the git user, or malicious CI pipeline definitions injected into high-value repositories
  4. Pipeline poisoning — modifying .gitlab-ci.yml or project settings to exfiltrate secrets during builds or push backdoored artifacts downstream

That last stage is where this becomes a supply-chain problem: anything built or deployed from a compromised GitLab instance is suspect.

Detection & Response

Assume breach if your instance was unpatched and internet-exposed during the exploitation window. Patching stops new exploitation; it does nothing about the shell an attacker dropped last week. Hunt before and after you patch.

Sigma Rules

The following rules target the two highest-fidelity post-exploitation behaviors: GitLab service processes spawning shells/interpreters, and web-accessible files being written into GitLab data directories.

YAML
---
title: GitLab Service Process Spawning Shell or Interpreter
id: 4f9c2e71-8b3a-4d5e-9f61-7a2c8e3b1d04
status: experimental
description: Detects GitLab web or worker components (puma, gitlab-workhorse, sidekiq, gitaly) spawning shells, interpreters, or download utilities — a strong indicator of post-exploitation activity following remote code execution on a GitLab server.
references:
  - https://www.bleepingcomputer.com/news/security/over-1-000-gitlab-servers-vulnerable-to-code-execution-attacks/
  - https://attack.mitre.org/techniques/T1059/
author: Security Arsenal
date: 2026/01/15
tags:
  - attack.execution
  - attack.t1059.004
  - attack.t1190
logsource:
  category: process_creation
  product: linux
detection:
  selection_parent:
    ParentImage|contains:
      - 'puma'
      - 'gitlab-workhorse'
      - 'sidekiq'
      - 'gitaly'
      - 'gitlab-rails'
  selection_child:
    Image|endswith:
      - '/sh'
      - '/bash'
      - '/dash'
      - '/python'
      - '/python3'
      - '/perl'
      - '/curl'
      - '/wget'
      - '/nc'
      - '/ncat'
  condition: selection_parent and selection_child
falsepositives:
  - Legitimate GitLab CI jobs execute as the runner user, not under puma/workhorse — executions parented to gitlab-runner are excluded by this logic
  - Rare administrative hooks; investigate rather than whitelist broadly
level: high
---
title: Executable or Script Written to GitLab Web-Accessible Directories
id: 8d1b3f52-6c47-4a89-b2e5-9f3d1a7c4e06
status: experimental
description: Detects creation of scripts or executables in GitLab upload and public directories, consistent with webshell staging after exploitation of a GitLab code execution vulnerability.
references:
  - https://www.bleepingcomputer.com/news/security/over-1-000-gitlab-servers-vulnerable-to-code-execution-attacks/
  - https://attack.mitre.org/techniques/T1505/003/
author: Security Arsenal
date: 2026/01/15
tags:
  - attack.persistence
  - attack.t1505.003
logsource:
  category: file_event
  product: linux
detection:
  selection_path:
    TargetFilename|contains:
      - '/var/opt/gitlab/gitlab-rails/uploads/'
      - '/var/opt/gitlab/gitlab-rails/shared/'
      - '/opt/gitlab/embedded/service/gitlab-rails/public/'
      - '/srv/gitlab/data/gitlab-rails/uploads/'
  selection_ext:
    TargetFilename|endswith:
      - '.sh'
      - '.php'
      - '.py'
      - '.pl'
      - '.rb'
      - '.elf'
  condition: selection_path and selection_ext
falsepositives:
  - GitLab upgrades and legitimate backup jobs; correlate with package manager activity and maintenance windows
level: high

KQL — Microsoft Sentinel / Defender

Even though GitLab runs on Linux, Sentinel ingests it cleanly via Syslog/CEF (auditd, Sysmon for Linux). This query hunts for the same shell-spawn behavior plus outbound connections from the git service account, which should essentially never initiate interactive outbound sessions.

KQL — Microsoft Sentinel / Defender
// Hunt: GitLab service processes spawning shells or making suspicious outbound connections
let gitlab_parents = dynamic(["puma", "gitlab-workhorse", "sidekiq", "gitaly", "gitlab-rails"]);
let shell_children = dynamic(["sh", "bash", "dash", "python", "python3", "perl", "curl", "wget", "nc", "ncat", "socat"]);
union isfuzzy=true
    (
    Syslog
    | where Facility =~ "auth" or SyslogMessage has_any (gitlab_parents)
    | where SyslogMessage has_any (shell_children) and SyslogMessage has_any (gitlab_parents)
    | extend Host = Computer, Activity = SyslogMessage
    | project TimeGenerated, Host, Activity, SeverityLevel
    ),
    (
    DeviceProcessEvents
    | where InitiatingProcessName has_any (gitlab_parents) or InitiatingProcessCommandLine has_any (gitlab_parents)
    | where FileName in~ (shell_children)
    | extend Host = DeviceName, Activity = strcat(InitiatingProcessCommandLine, " -> ", ProcessCommandLine)
    | project TimeGenerated, Host, Activity, AccountName
    ),
    (
    DeviceNetworkEvents
    | where InitiatingProcessAccountName in~ ("git", "gitlab-www", "gitlab-redis")
    | where RemoteIP !startswith "10." and RemoteIP !startswith "192.168." and RemoteIP !startswith "172.16."
    | where RemotePort in (443, 80, 4444, 5555, 1337, 22)
    | extend Host = DeviceName, Activity = strcat(InitiatingProcessCommandLine, " -> ", RemoteIP, ":", RemotePort)
    | project TimeGenerated, Host, Activity, RemoteIP, RemotePort
    )
| order by TimeGenerated desc

Tune the RFC1918 exclusions and ports to your egress baseline — GitLab legitimately reaches package mirrors and container registries, so focus review on uncommon ports and unknown destinations, and on any shell spawned under a web worker.

Velociraptor VQL

Deploy this hunt across your Linux fleet (or scope it to known GitLab hosts) to identify shells under GitLab services, suspicious outbound connections from the git user, and recently dropped scripts in GitLab data paths.

VQL — Velociraptor
-- Hunt for post-exploitation artifacts on GitLab servers:
-- shells/interpreters under GitLab services, scripts in data dirs, outbound conns from git user
SELECT Pid, Ppid, Name, Exe, CommandLine, Username, CreateTime
FROM pslist()
WHERE (Exe =~ '(puma|gitlab-workhorse|sidekiq|gitaly|gitlab-rails)'
       AND CommandLine =~ '(sh|bash|python|perl|curl|wget|nc |ncat|socat)')
   OR (Username =~ '^(git|gitlab-www)$'
       AND Name =~ '(sh|bash|dash|python|perl|nc|ncat|socat)')
VQL — Velociraptor
-- Sweep GitLab upload/public directories for recently created script files (potential webshells)
SELECT FullPath, Size, Mtime, Ctime
FROM glob(globs=['/var/opt/gitlab/gitlab-rails/uploads/**/*.sh',
                 '/var/opt/gitlab/gitlab-rails/uploads/**/*.php',
                 '/var/opt/gitlab/gitlab-rails/uploads/**/*.py',
                 '/var/opt/gitlab/gitlab-rails/shared/**/*.rb',
                 '/opt/gitlab/embedded/service/gitlab-rails/public/**/*.sh'])
WHERE Mtime > (now() - 1209600)  -- files modified in the last 14 days
ORDER BY Mtime DESC
VQL — Velociraptor
-- Identify outbound network connections owned by GitLab service accounts
SELECT Pid, Name, Username, Family, Type, Status, Laddr, Lport, Raddr, Rport
FROM netstat()
WHERE Username =~ '^(git|gitlab-www|gitlab-redis)$'
  AND Status =~ 'ESTABLISHED'
  AND Raddr !~ '^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|127\.)'

Remediation & Verification Script (Bash)

Run on each self-managed GitLab host. It reports current version against the latest available, pulls the installed package inventory, and surfaces the most common persistence artifacts we see after GitLab compromise — before you patch, so you preserve evidence.

Bash / Shell
#!/bin/bash
# GitLab compromise triage + patch verification — run as root
echo "=== [1] Installed GitLab version ==="
if command -v gitlab-rake >/dev/null 2>&1; then
  gitlab-rake gitlab:env:info 2>/dev/null | head -20
fi
apt list --installed 2>/dev/null | grep -i gitlab || rpm -qa | grep -i gitlab

echo "=== [2] Available upgrade target (do NOT auto-install during triage) ==="
apt-cache policy gitlab-ee gitlab-ce 2>/dev/null || yum list available gitlab-ee gitlab-ce 2>/dev/null

echo "=== [3] Shells spawned by GitLab service accounts (last 500 auth/audit lines) ==="
grep -E "(git|gitlab-www)" /var/log/auth.log /var/log/secure 2>/dev/null | grep -E "(session opened|COMMAND)" | tail -50

echo "=== [4] Persistence check: cron, systemd, SSH keys for git user ==="
crontab -l -u git 2>/dev/null
ls -la /var/spool/cron/ 2>/dev/null | grep -E "git"
systemctl list-units --type=service --state=running 2>/dev/null | grep -iE "(gitlab-backdoor|update|cache|sync)" | grep -viE "gitlab"
stat -c '%y %n' /var/opt/gitlab/.ssh/authorized_keys /home/git/.ssh/authorized_keys 2>/dev/null

echo "=== [5] Recently modified scripts in GitLab data directories (14 days) ==="
find /var/opt/gitlab/gitlab-rails/uploads /var/opt/gitlab/gitlab-rails/shared \
     /opt/gitlab/embedded/service/gitlab-rails/public \
     -type f \( -name '*.sh' -o -name '*.php' -o -name '*.py' -o -name '*.rb' \) \
     -mtime -14 2>/dev/null | head -50

echo "=== [6] Outbound connections from git/gitlab service accounts ==="
ss -tnp 2>/dev/null | grep -E "users:\(\(\"(bash|sh|python|perl|nc)" 

echo "=== [7] GitLab integrity self-check ==="
gitlab-rake gitlab:check SANITIZE=true 2>/dev/null | tail -30
echo "=== Triage complete. If sections 3-6 show unexpected results, isolate the host BEFORE patching. ==="

After triage, patch with your package manager (apt-get install gitlab-ce / gitlab-ee at the fixed version per GitLab's security release notes, or pull the fixed Docker image tag), then re-run sections 3–6 to confirm no residual artifacts.

Remediation

  1. Patch immediately. Upgrade all self-managed GitLab CE/EE instances to the latest security release per GitLab's official security releases page: https://about.gitlab.com/releases/categories/releases/ and the GitLab security advisories index. Do not rely on a partially applied patch train — go to the latest patch release for your major version at minimum.
  2. Treat exposure as compromise. Any instance that was internet-reachable and unpatched during the active exploitation window should undergo compromise assessment (script above, plus VQL hunting) before being returned to service. Preserve logs and disk images first.
  3. Remove internet exposure where possible. GitLab rarely needs to be fully public. Place it behind a VPN or zero-trust access gateway, restrict source IPs at the edge, and enforce SSO/MFA for all accounts. If public access is a hard requirement, front it with a WAF and rate limiting.
  4. Rotate everything on a confirmed or suspected compromise: CI/CD variables and deploy tokens, runner registration and authentication tokens, OAuth application secrets, gitlab-secrets.json contents, database credentials, and any cloud/API keys stored in pipelines. Audit .gitlab-ci.yml and project webhooks/integrations across all repositories for unauthorized changes.
  5. Audit user and access tokens. Enumerate recently created accounts, personal access tokens, and project/group membership changes — unauthenticated RCE is frequently used to mint durable access before defenders patch.
  6. Establish version drift alerting. Add GitLab version monitoring to your vulnerability management program so instances never sit more than one security release behind. Internet-exposed assets carrying maximum-severity flaws should be on a 24–72 hour patch SLA, consistent with CISA's remediation expectations for actively exploited vulnerabilities.
  7. Segment and monitor. GitLab hosts should have egress filtering (deny-by-default outbound, allowlisting package mirrors and registries) and full process/network telemetry forwarded to your SIEM — the detections above are useless without Sysmon for Linux, auditd, or equivalent collection in place.

Related Resources

Security Arsenal Penetration Testing Services AlertMonitor Platform Book a SOC Assessment vulnerability-management Intel Hub

Is your security operations ready?

Get a free SOC assessment or see how AlertMonitor cuts through alert noise with automated triage.