← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
CVE-2026-5078 · CWE-117

morgan vulnerable to Log Forging via unneutralized control characters in :remote-user

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
01 · The Real Story

This is someone scribbling fake entries into your guestbook, not picking the lock on the building

In morgan, the :remote-user token is populated from the HTTP Authorization: Basic ... header via the basic-auth package, and the current upstream source returns the decoded username directly into the log line without neutralizing control characters. Because the standard default, combined, common, and short formats all include :remote-user, an attacker who can send a request can base64-encode a username containing newline or other control characters and poison line-oriented logs. *Inference from public source code:* the latest public release is 1.10.1, and the current public source still shows the unsafe pattern, so the safest external assessment is that public releases through 1.10.1 are affected unless the operator has locally sanitized or removed :remote-user.

The vendor's MEDIUM 5.3 is defensible in a vacuum because the bug is remotely reachable and changes integrity, but it still overshoots enterprise patch urgency. This is not initial access, not auth bypass, and not code execution; it's audit-trail corruption whose real damage depends on whether you use a vulnerable log format, trust those logs operationally, and have downstream systems that treat a forged line as truth. For a 10,000-host program, that's *backlog hygiene with some SOC-facing compensations*, not a front-of-queue fire.

"Remote log poisoning, not remote compromise: patch it, but don't let a CWE-117 label steal Monday morning."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Send a crafted Basic auth header

The attacker uses curl, Burp Repeater, or any HTTP client to send an Authorization: Basic ... header where the decoded username contains control characters such as \n, \r, or terminal escapes. The application does not need to use Basic auth for access control; the header only needs to reach the Node/Express process.
Conditions required:
  • Network reachability to the Node/Express app
  • Ability to set arbitrary HTTP headers
  • Morgan enabled on the request path
Where this breaks in practice:
  • Some proxies, API gateways, or edge services strip or normalize Authorization headers
  • If the app never logs the request path in question, the attack dies here
Detection/coverage: DAST coverage is weak; most scanners do not test control characters inside base64-decoded Basic usernames.
STEP 02

Decode attacker-controlled username

Morgan calls basic-auth(req), and basic-auth base64-decodes the credentials then splits on the first colon. The resulting credentials.name can contain non-printable characters because the parser does not neutralize them before returning the username.
Conditions required:
  • The request includes syntactically valid Basic credentials
  • Morgan's :remote-user token is evaluated
Where this breaks in practice:
  • Malformed Basic headers are ignored
  • A username cannot contain the delimiter colon, so the payload space is slightly constrained
Detection/coverage: SAST can catch this pattern if rules look for CWE-117 or unsafe logging of decoded auth material; many package scanners will lag because public advisory metadata is sparse.
STEP 03

Inject control characters into the log line

Morgan's predefined default, combined, common, and short formats all interpolate :remote-user into a single line. Because the value is not escaped, control characters can terminate the current line, create fake follow-on entries, or contaminate terminal output and naive log shippers.
Conditions required:
  • Deployment uses a format containing :remote-user or a custom format that includes it
  • Logs are written to stdout, files, or a collector that preserves the injected characters
Where this breaks in practice:
  • Teams using dev or tiny are not hit by this specific token path
  • Structured logging pipelines that re-encode fields can blunt the effect
Detection/coverage: Hunt for control characters in access logs, especially lines with Authorization-driven usernames, malformed timestamps, or impossible client/user combinations.
STEP 04

Mislead investigators or downstream automation

The practical outcome is forged or ambiguous audit evidence: fake request lines, broken parsers, and SOC confusion during triage. In the worst realistic case, a brittle downstream parser or alerting rule treats the forged line as a real event, but this CVE by itself does not provide host takeover.
Conditions required:
  • Operators rely on raw Morgan access logs for forensic truth or alerting
  • Downstream consumers do not validate record boundaries
Where this breaks in practice:
  • Most mature SIEM pipelines enrich from multiple sources, limiting blast radius
  • The attacker still cannot alter application state beyond what a normal request already could
Detection/coverage: Runtime detection is mostly operational: look for newline-bearing usernames, ANSI escapes, or impossible duplicate timestamps within a single request flow.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusNo public exploitation evidence found in the sources reviewed, and it is not listed in the CISA KEV catalog.
Proof-of-concept availabilityNo dedicated public PoC repo located. Practical exploitation is trivial with curl/Burp by sending a crafted Basic username, so the *absence of a repo is not meaningful friction*.
EPSSNo public EPSS value was located in the reviewed sources for this CVE at time of assessment; treat that as missing data, not proof of safety.
KEV statusNot KEV-listed; no CISA due date applies.
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N — the vendor score captures remote reachability and low integrity impact, but it does not capture the real-world limiter that this is only log integrity, not system compromise.
Affected versions*Inference from public code and release history:* public upstream releases through 1.10.1 appear affected because the latest release and current public source still return credentials.name unsanitized.
Fixed versionsNo public patched release or vendor advisory was found in the reviewed sources. No distro backport guidance located.
Exposure / scanning realityThis is an application-library issue, not a bannered network appliance bug. Shodan/Censys/FOFA-style external counting is largely *not applicable*; exposure must be measured from SBOMs, package-lock.json, and application inventories.
Disclosure / attributionNVD had no record per the prompt, and no public upstream advisory with researcher credit was found. Public attribution is therefore unclear from the sources reviewed.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to LOW (3.4/10)

The decisive downgrade is that exploitation stops at log integrity manipulation; it does not create code execution, privilege gain, or authentication bypass. Even though the attacker position is unauthenticated remote, the blast radius is narrowed by a second-order dependency on logging format, log trust, and downstream parsing behavior.

HIGH Impact is limited to log/audit integrity rather than host compromise
MEDIUM Reachability depends on whether deployments use `:remote-user` in their Morgan format
LOW Public fixed-version metadata is incomplete or absent

Why this verdict

  • Dropped from the 5.3 vendor baseline because the effect is post-request log poisoning, not attacker progression — no foothold expansion, no session theft, no code execution.
  • Real deployment exposure is narrower than AV:N suggests — only apps running Morgan *and* using a format containing :remote-user are affected; dev and tiny are not hit by this exact path.
  • No active exploitation signal — no KEV listing, no public campaign reporting, and no public exploit ecosystem beyond trivial manual reproduction.

Why not higher?

An unauthenticated remote attacker can reach the vulnerable code path, but the consequence is forged log content, not direct system compromise. To get from 'poisoned access log' to 'material enterprise damage,' you need additional assumptions about analyst trust, downstream parser weakness, or brittle automation.

Why not lower?

This still matters because the unsafe token is present in several standard Morgan formats, and attackers do not need the app to actually authenticate them with Basic auth to influence :remote-user. If your access logs feed investigations, detections, or compliance evidence, forged lines are a real integrity problem.

05 · Compensating Control

What to do — in priority order.

  1. Remove :remote-user from Morgan formats — If you cannot patch or upstream has not shipped a fix, switch default/combined/common/short to a custom format that omits :remote-user. For a LOW verdict there is no SLA; treat this as backlog hygiene and make the change during normal application maintenance.
  2. Sanitize log fields before write — Neutralize CR, LF, tabs, and ANSI/control bytes in any user-derived token before writing to stdout or files. For this LOW issue, implement during normal logger hardening rather than emergency change windows.
  3. Prefer structured logging — Move access telemetry into JSON with proper field encoding, then let the shipper serialize records instead of concatenating line-oriented strings. This is the best long-term control for CWE-117 style bugs and fits backlog-hygiene timing.
  4. Alert on control characters in access logs — Add a SIEM or log-pipeline rule to flag usernames or request fields containing non-printable bytes, line breaks, or escape sequences. There is no emergency deadline here, but it is a cheap detective control for shared Express logging patterns.
What doesn't work
  • MFA does not help because the issue is not account takeover; the attacker only needs to send a crafted header.
  • A generic WAF rarely helps much because the payload is hidden inside valid base64 Basic credentials and the dangerous behavior happens after decoding in the app.
  • Simply disabling Basic auth in the application does not reliably help if Morgan still parses a supplied Authorization header and logs :remote-user.
06 · Verification

Crowdsourced verification payload.

Run this on the application host, CI workspace, or source checkout that contains the Node project. Invoke it with python3 check_morgan_cve_2026_5078.py /path/to/app using any account that can read the project files; no admin rights are required.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/usr/bin/env python3
"""
check_morgan_cve_2026_5078.py

Detect likely exposure to CVE-2026-5078 in the Node.js `morgan` package.

Usage:
  python3 check_morgan_cve_2026_5078.py /path/to/app

Exit codes:
  0 = PATCHED / not affected
  1 = VULNERABLE
  2 = UNKNOWN / insufficient evidence
"""

import json
import os
import re
import sys
from typing import List, Tuple

FIXED_VERSION = None  # No public fixed version was located during assessment


def parse_semver(v: str):
    m = re.match(r'^(\d+)\.(\d+)\.(\d+)', v or '')
    if not m:
        return None
    return tuple(int(x) for x in m.groups())


def find_morgan_paths(root: str) -> List[str]:
    hits = []
    for dirpath, dirnames, filenames in os.walk(root):
        if os.path.basename(dirpath) == 'morgan':
            pkg = os.path.join(dirpath, 'package.json')
            idx = os.path.join(dirpath, 'index.js')
            if os.path.isfile(pkg) and os.path.isfile(idx):
                hits.append(dirpath)
    return sorted(set(hits))


def load_pkg_version(pkg_json: str):
    try:
        with open(pkg_json, 'r', encoding='utf-8') as f:
            data = json.load(f)
        return data.get('version')
    except Exception:
        return None


def source_looks_vulnerable(index_js: str) -> bool:
    try:
        with open(index_js, 'r', encoding='utf-8', errors='replace') as f:
            src = f.read()
    except Exception:
        return False

    token_present = "morgan.token('remote-user'" in src or 'morgan.token("remote-user"' in src
    unsafe_return = 'return credentials ? credentials.name : undefined' in src
    sanitize_markers = [
        'replace(/\\r/g',
        'replace(/\\n/g',
        'replace(/[\\r\\n]/g',
        'replace(/[\\x00-\\x1f\\x7f]/g',
        'sanitize',
        'escape'
    ]
    sanitization_obvious = any(marker in src for marker in sanitize_markers)
    return token_present and unsafe_return and not sanitization_obvious


def assess_one(path: str) -> Tuple[str, str]:
    pkg = os.path.join(path, 'package.json')
    idx = os.path.join(path, 'index.js')
    version = load_pkg_version(pkg)
    looks_vuln = source_looks_vulnerable(idx)

    if looks_vuln:
        return ('VULNERABLE', f'{path} version={version or "unknown"} source pattern matches unsanitized :remote-user token')

    if version and FIXED_VERSION:
        pv = parse_semver(version)
        fv = parse_semver(FIXED_VERSION)
        if pv and fv and pv >= fv:
            return ('PATCHED', f'{path} version={version} >= fixed version {FIXED_VERSION}')

    if version and not looks_vuln:
        return ('UNKNOWN', f'{path} version={version} found, but source did not clearly match the known vulnerable pattern and no public fixed version is available')

    return ('UNKNOWN', f'{path} found, but version/source inspection was inconclusive')


def main():
    if len(sys.argv) != 2:
        print('UNKNOWN - usage: python3 check_morgan_cve_2026_5078.py /path/to/app')
        sys.exit(2)

    root = os.path.abspath(sys.argv[1])
    if not os.path.exists(root):
        print(f'UNKNOWN - path does not exist: {root}')
        sys.exit(2)

    paths = find_morgan_paths(root)
    if not paths:
        print('PATCHED - morgan package not found under the supplied path')
        sys.exit(0)

    results = [assess_one(p) for p in paths]
    for status, detail in results:
        print(f'{status} - {detail}')

    statuses = [s for s, _ in results]
    if 'VULNERABLE' in statuses:
        print('VULNERABLE')
        sys.exit(1)
    if all(s == 'PATCHED' for s in statuses):
        print('PATCHED')
        sys.exit(0)

    print('UNKNOWN')
    sys.exit(2)


if __name__ == '__main__':
    main()
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning: do not let this displace internet-facing RCEs or KEV work. For a LOW noisgate rating there is no noisgate mitigation SLA and no noisgate remediation SLA — treat it as backlog hygiene: identify apps using morgan with :remote-user, strip or sanitize that token in normal change windows, and then fold any eventual upstream fix into your standard dependency-update cadence rather than emergency patching.

Sources

  1. expressjs/morgan repository and README
  2. morgan current source (`index.js`)
  3. morgan latest public release 1.10.1
  4. Snyk package versions for morgan
  5. basic-auth source on UNPKG
  6. CISA Known Exploited Vulnerabilities Catalog
  7. MITRE CAPEC log forging pattern
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.