Skip to content
Open-Source Registry

pypi / dictmerge-lite

dictmerge-lite 1.0.0

bitbison: flagged, behavior flagged, dependencies clean, posture not checked, pipeline clean, 1 of 1 jobs captured RiskcriticalVulnsnone
processes
412
operations
22K
packages
0
compiler runs
3
connections
2
build time
13s
job time
13s
capture
187 KB
records
33K
bytes / record
5.7

Small recursive dictionary merge helper

Build behaviourcompiled and nothing more

published 2026-10-25 View on PyPI
Reviewed by Bitbison: maliciousPublished 25 October 2026 at 23:52 UTC; Bitbison caught it stealing credentials at 23:55 UTC. Importing the package reads SSH keys, cloud and registry credentials, .env files, browser password stores and crypto-wallet extension data, and uploads them to svbonlinebanking.com. setup.py carries the same collection for builds from source.

Graph

credential-sweep · critical
2

Checks

8 of 10 clear · 1 not checked Credential stores read
Build behavior✓✓▲✓✓○✓
Supply chain✓✓
Pipeline✓

Detections

1
  • Code this build ran read 18 credential stores it does not own, then connected to svbonlinebanking.com
    python3.14 › svbonlinebanking.com:443
    critical

Detection

criticalwitnessed

Code this build ran read 18 credential stores it does not own, then connected to svbonlinebanking.com

Many credential stores read by code that does not own them

Subject
python3.14
Destination
svbonlinebanking.com:443
Jobs
build-import
Rule
credential-sweep

Evidence

  1. Credential store read by code that does not own itcause

    Read /home/runner/.ssh/id_rsa (forges)

    source /home/runner/.ssh/id_rsa

    by /home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …

  2. Credential store read by code that does not own itcause

    Read /home/runner/.ssh/id_rsa.pub (scp)

    source /home/runner/.ssh/id_rsa.pub

    by /home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …

  3. Credential store read by code that does not own itcause

    Read /home/runner/.ssh/id_ed25519 (forges)

    source /home/runner/.ssh/id_ed25519

    by /home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …

  4. Credential store read by code that does not own itcause

    Read /home/runner/.ssh/id_ed25519.pub (scp)

    source /home/runner/.ssh/id_ed25519.pub

    by /home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …

  5. Credential store read by code that does not own itcause

    Read /home/runner/.ssh/config (forges)

    source /home/runner/.ssh/config

    by /home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …

  6. Credential store read by code that does not own itcause

    Read /home/runner/.aws/credentials (cloud)

    source /home/runner/.aws/credentials

    by /home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …

  7. Credential store read by code that does not own itcause

    Read /home/runner/.aws/config (cloud)

    source /home/runner/.aws/config

    by /home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …

  8. Credential store read by code that does not own itcause

    Read /home/runner/.config/gcloud/application_default_credentials.json (cloud)

    source /home/runner/.config/gcloud/application_default_credentials.json

    by /home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …

  9. Credential store read by code that does not own itcause

    Read /home/runner/.azure/accessTokens.json (cloud)

    source /home/runner/.azure/accessTokens.json

    by /home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …

  10. Credential store read by code that does not own itcause

    Read /home/runner/.azure/azureProfile.json (cloud)

    source /home/runner/.azure/azureProfile.json

    by /home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …

  11. Credential store read by code that does not own itcause

    Read /home/runner/.npmrc (registries)

    source /home/runner/.npmrc

    by /home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …

  12. Credential store read by code that does not own itcause

    Read /home/runner/.git-credentials (forges)

    source /home/runner/.git-credentials

    by /home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …

  13. Credential store read by code that does not own itcause

    Read /home/runner/.pgpass (databases)

    source /home/runner/.pgpass

    by /home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …

  14. Credential store read by code that does not own itcause

    Read /home/runner/.my.cnf (databases)

    source /home/runner/.my.cnf

    by /home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …

  15. Credential store read by code that does not own itcause

    Read /home/runner/.kube/config (containers)

    source /home/runner/.kube/config

    by /home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …

  16. Credential store read by code that does not own itcause

    Read /home/runner/.docker/config.json (containers)

    source /home/runner/.docker/config.json

    by /home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …

  17. Credential store read by code that does not own itcause

    Read /home/runner/.config/gh/hosts.yml (forges)

    source /home/runner/.config/gh/hosts.yml

    by /home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …

  18. Credential store read by code that does not own itcause

    Read the project's own credentials (project)

    source /home/runner/work/dictmerge-lite/dictmerge-lite/.env

    by /home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …

  19. Connected after reading a credential storecause

    python3.14 connected to svbonlinebanking.com:443 after the reads

    target svbonlinebanking.com:443

    by /home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …

  20. Why it matterscontext

    A program reads its own credentials. This build read many credential stores it does not own, of several kinds, which is how a credential stealer collects what it sends out.

What came before it

Loading the graph

Processes

build-import

python›python

Ran
/home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …
Invoked by
.venv/bin/python /home/runner/work/_temp/imports.py dictmerge-lite
Lineage · 2 processes
  1. python.venv/bin/python /home/runner/work/_temp/imports.py dictmerge-lite3842
  2. python/home/runner/work/dictmerge-lite/dictmerge-lite/.venv/bin/python -c import importlib, pkgutil, sys m = importlib.import_module(sys.argv[1]) count = failed = 0 …3845

Remediation

Find the code that read these stores and why: a package's install, build or import step has no reason to read credentials it does not own. Treat every credential these stores hold as exposed, and rotate them unless the reads are explained.

Network

2 peers
  • svbonlinebanking.com:443 dictmerge-lite's own build code · python3.14 · SKN Subnet & Telecom Ltd unjudged
  • files.pythonhosted.org:8080 dictmerge-lite's own build code · python3.14 via Package registries profile (pip → files.pythonhosted.org) ecosystem
Versions1RiskVulnsPublished
1.0.0criticalnone2h ago

Raw data

capture
187 KB
records
32,984
bytes per record
5.7

Loading a sample of the capture

Want to see more? E-mail us at hello@bitbison.io about your use case.

1 of 1 jobs captured: build-import success · Generated 2026-10-25 · run 0c4ca70e-992d-4124-aa5a-7696b1dde5b0

analysis 0.1.0 · 2026-10-24 · f9e9f06 · graph v2

dictmerge-lite steals credentials, browser logins and crypto wallets when it is imported

maliciousCovers 1.0.0Reviewed by BitbisonUpdated 2026-10-26

Published 25 October 2026 at 23:52 UTC; Bitbison caught it stealing credentials at 23:55 UTC. Importing the package reads SSH keys, cloud and registry credentials, .env files, browser password stores and crypto-wallet extension data, and uploads them to svbonlinebanking.com. setup.py carries the same collection for builds from source.

dictmerge-lite 1.0.0 was uploaded to PyPI on 25 October 2026 at 23:52:16 UTC. It presents itself as a small recursive dictionary merge helper. It is a credential and crypto-wallet stealer. Importing the package collects the machine's credentials, browser logins and wallet data and sends them to svbonlinebanking.com.

Bitbison caught the release stealing credentials at 23:55:36 UTC, 3 minutes 20 seconds after it was published. Bitbison's analysis of the release flags it as critical.

What Bitbison saw

When the package was imported, one Python process did the following in 0.45 seconds:

  1. Wrote the marker /tmp/.dw-c00ffc8c67acc982.
  2. Read ~/.ssh/id_rsa, ~/.ssh/id_rsa.pub, ~/.ssh/id_ed25519, ~/.ssh/id_ed25519.pub and ~/.ssh/config.
  3. Read ~/.aws/credentials, ~/.aws/config, the Google Cloud application default credentials, and the Azure accessTokens.json and azureProfile.json.
  4. Read ~/.npmrc, ~/.pypirc, ~/.git-credentials, ~/.gitconfig, ~/.pgpass, ~/.my.cnf, ~/.kube/config, ~/.docker/config.json and the GitHub CLI's ~/.config/gh/hosts.yml.
  5. Read the project's .env and the .env one directory above it.
  6. Resolved svbonlinebanking.com, connected to it at 209.99.190.173 on port 443, uploaded what it had read and received the server's answer.

What the payload does

The package is two files. __init__.py holds a 15-line merge() helper and, before it, an import of _telemetry and a call that starts it in a background thread. The thread is not a daemon thread, so the upload finishes even when the program that imported the package exits at once. Any error is swallowed.

_telemetry.py calls itself a "dependency telemetry collector". It collects:

  • Developer and cloud credentials: SSH keys and config, AWS, Google Cloud and Azure credentials, .npmrc, .pypirc, .git-credentials, .gitconfig, .netrc, .pgpass, .my.cnf, the Kubernetes and Docker configs and the GitHub CLI's token.
  • Shell and REPL histories: bash, zsh, Python, Node, psql and mysql.
  • Browser logins: the saved passwords and form data of Chrome, Chromium, Brave, Edge and Opera, with each browser's Local State, and Firefox's logins.json, key4.db and cert9.db.
  • Crypto wallets: the extension storage of MetaMask, Phantom, Rabby, Keplr, TronLink, Coinbase Wallet, Trust Wallet and Solflare in every Chrome-family profile.
  • Secret files: any .env, secrets.*, credentials.json, service-account*.json, *.pem, *.key, *.p12 or *.pfx, and private keys, up to three directories below the working directory and the home directory.

It takes files up to 512 KB, wallet files up to 2 MB, and up to 24 MB in all. It adds the hostname, user name, operating system, working directory and Python version, gzips the bundle and encrypts it with AES-GCM when the cryptography package is installed. It signs the request with HMAC-SHA256 and POSTs it to https://svbonlinebanking.com/v1/beacon with the headers x-dw-campaign: main, x-dw-ts, x-dw-sig and x-dw-enc. It tries twice.

It runs on Linux, macOS and Windows. The marker in the temporary directory stops it from collecting again for 15 minutes. It does nothing when DW_OFF=1 is set or ~/.dw_off exists.

setup.py overrides setuptools' build_py and install commands to run the same collection, so a build from the source distribution can trigger it too.

Is it malicious?

Yes. Nothing in a dictionary merge helper needs to read SSH keys, cloud credentials, browser password stores or wallet data, and the package sends what it reads to a server unrelated to its purpose. The collection is hidden behind a "telemetry" name, runs on import and at build time, and hides every error. The collection server's name imitates a bank's. The release names its author only as "Nick", with no homepage or repository.

Who is affected

  • Anyone who imported dictmerge-lite 1.0.0. That includes a test run, a script, or a tool that imports every module of its environment.
  • Anyone who built or installed it from the source distribution, since setup.py carries the same hooks.
  • Anything built with it. An image or virtual environment with the package inside runs the payload each time a program imports it.

Every credential, browser login and wallet that an affected process could read is at risk. A CI runner's secrets and cloud role are among them.

What to do

  • Search lockfiles, images and environments for dictmerge-lite. Look for /tmp/.dw-* markers too.
  • Uninstall it. Do not import it to check what it is.
  • Rotate every credential the affected user account could read: SSH keys, cloud keys, registry and forge tokens, database passwords and the secrets in .env files. Change the passwords saved in its browsers.
  • Move the funds of any wallet whose extension data was on the machine to a new wallet.
  • Block svbonlinebanking.com and 209.99.190.173.

Indicators

IndicatorValue
Packagedictmerge-lite 1.0.0 on PyPI
Source distributiondictmerge_lite-1.0.0.tar.gz, sha256 a876828504e992d60f14b5e904eddb8175dda09309b3785d24f0546236469197
Wheeldictmerge_lite-1.0.0-py3-none-any.whl, sha256 46d23a8b05ceb5890990d37c8e3f68a98ecec03dd7f133df85407d2831fb712a
Payloaddictmerge_lite/_telemetry.py, sha256 3d6e0a92f51d601e9b26135eafcc4512994cb658b5f87539ccfb0c6158ba42c3
Collection serverhttps://svbonlinebanking.com/v1/beacon, at 209.99.190.173 (AS402253, SKN Subnet & Telecom Ltd)
Request headersx-dw-campaign: main, x-dw-ts, x-dw-sig, x-dw-enc
User agentdictmerge-lite/1.0.0
Marker/tmp/.dw-<16 hex digits>

Timeline

All times are UTC, 25 October 2026.

WhenWhat happened
23:52:16dictmerge-lite 1.0.0 is uploaded to PyPI, wheel and source distribution.
23:55:36.284The payload runs. It writes its marker and reads the machine's credential stores.
23:55:36.567The payload connects to svbonlinebanking.com and uploads the credentials.
23:55:36.726The server answers. Bitbison flags the release.

Sources

Indicators

hosts
svbonlinebanking.com
addresses
209.99.190.173:443
requests
POST /v1/beacon
files
dictmerge_lite/_telemetry.py/tmp/.dw-*~/.dw_off

Examined

1.0.0
sha256:a876828504e992d60f14b5e904eddb8175dda09309b3785d24f0546236469197

Links