Back to Blog
critical SEVERITY8 min read

How Unauthenticated API Endpoints happen in Node.js Express and how to fix it

The `/token` endpoint in `plugin/multiplex/index.js` generated presentation control tokens without verifying the requester's identity, allowing any attacker with network access to seize control of a live reveal.js presentation. The fix restricts token generation to localhost-only requests and replaces a broken cryptographic primitive with a proper SHA-256 hash. Together, these changes eliminate both the access-control gap and a secondary cryptographic weakness in a single targeted patch.

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

This is an unauthenticated API endpoint vulnerability (CWE-306: Missing Authentication for Critical Function) in the reveal.js multiplex plugin's Express server (`plugin/multiplex/index.js`). Any network-reachable attacker could call `GET /token` to obtain a valid presentation control token with no credentials required, then use that token to drive slide navigation and read speaker notes. The fix adds an IP allowlist check that restricts `/token` to `127.0.0.1` / `::1` / `::ffff:127.0.0.1` (localhost only) and returns HTTP 403 to all other callers. A secondary fix replaces the deprecated and misused `crypto.createCipher('blowfish', secret)` call with `crypto.createHash('sha256').update(secret).digest('hex')`, removing a cryptographic weakness that produced no meaningful output.

Vulnerability at a Glance

cweCWE-306
fixRestrict the `/token` endpoint to localhost-only callers; return HTTP 403 to all remote addresses
riskAny network-reachable attacker can obtain a presentation control token and hijack slide navigation or read speaker notes
languageJavaScript (Node.js)
root causeThe Express `/token` route generated and returned a secret token with no identity check on the caller
vulnerabilityMissing Authentication for Critical Function

The Vulnerability in Context

The plugin/multiplex/index.js file is the server-side component of reveal.js's multiplex plugin — the feature that lets a presenter's browser drive the slide deck seen by a remote audience. It runs a small Express HTTP server that exposes two endpoints: / (serves presentation content) and /token (generates the secret that grants presenter-level control). The token endpoint is the crown jewel: whoever holds that value can push slide changes to every connected viewer.

The problem? Until this fix, anyone on the network could call GET /token and receive a valid presenter token with zero authentication required.

This post walks through exactly what the vulnerable code looked like, how an attacker would exploit it, and what the two-part fix does to close the gap.


The Vulnerability Explained

The Unauthenticated /token Endpoint

Here is the vulnerable route handler as it existed before the patch (starting around line 43 of plugin/multiplex/index.js):

// BEFORE — no authentication whatsoever
app.get("/token", function(req, res) {
    var ts = new Date().getTime();
    var rand = Math.floor(Math.random() * 9999999);
    var secret = ts.toString() + rand.toString();
    res.send(createHash(secret));
});

There is no middleware, no session check, no API key validation, and no IP restriction. The handler fires unconditionally for every GET /token request regardless of who is asking.

The createHash function that produces the token had its own problem:

// BEFORE — broken cryptographic call
var createHash = function(secret) {
    var cipher = crypto.createCipher('blowfish', secret);
    return(cipher.final('hex'));
};

crypto.createCipher is a symmetric encryption function, not a hash function. Calling .final() without ever calling .update() means no data is passed through the cipher — the output is an artifact of the cipher's finalization step on empty input, not a meaningful transformation of secret. Node.js deprecated crypto.createCipher in v10 and removed it in v22 precisely because it uses a weak key-derivation step (MD5, no salt). The resulting "token" is both cryptographically weak and reproducible in ways the author did not intend.

How an Attacker Exploits This

The attack chain is two steps and requires nothing beyond curl:

Step 1 — Retrieve the token:

curl http://target-host:1948/token
# Response: a3f9e1c2b4d6...  (the presenter secret)

Step 2 — Use the token to control the presentation:

The multiplex plugin's client-side JavaScript connects to the Socket.IO server and listens for events authenticated by the token. With the token in hand, an attacker can emit slidechanged events to push arbitrary slide positions to every viewer's browser, effectively hijacking the presentation in real time.

Because the /token endpoint has no rate limiting either, an attacker can also harvest tokens continuously to ensure they always have a current valid credential.

Real-World Impact

  • Presentation hijacking: An attacker in the same network (conference Wi-Fi, corporate LAN, or any internet-exposed deployment) can take over slide navigation during a live talk.
  • Speaker notes exposure: Depending on the multiplex configuration, the token also gates access to speaker notes — private content not meant for the audience.
  • Downstream consumers: reveal.js is a library. Every project that bundles this plugin and exposes port 1948 inherits this vulnerability.

The Fix

The patch makes two distinct changes to plugin/multiplex/index.js.

Change 1: Localhost-Only Restriction on /token

// AFTER — IP allowlist added before token generation
app.get("/token", function(req, res) {
    var ip = req.ip || req.connection.remoteAddress;
    if (ip !== '127.0.0.1' && ip !== '::1' && ip !== '::ffff:127.0.0.1') {
        res.status(403).send('Forbidden');
        return;
    }
    var ts = new Date().getTime();
    var rand = Math.floor(Math.random() * 9999999);
    var secret = ts.toString() + rand.toString();
    res.send(createHash(secret));
});

The fix reads the caller's IP address from req.ip (Express's resolved address, respecting trust proxy settings) with a fallback to req.connection.remoteAddress. It then checks against all three loopback representations:

Value Meaning
'127.0.0.1' IPv4 loopback
'::1' IPv6 loopback
'::ffff:127.0.0.1' IPv4-mapped IPv6 loopback

Any caller whose IP does not match one of these values receives HTTP 403 Forbidden and the function returns immediately. The token generation code is never reached.

This is the correct threat model for a token-generation endpoint: only the presenter (running locally) should ever need to call /token. Remote audiences connect to the Socket.IO endpoint using a token the presenter shares with them out-of-band — they have no legitimate reason to call /token themselves.

Change 2: Replace Broken Cryptography with SHA-256

- var cipher = crypto.createCipher('blowfish', secret);
- return(cipher.final('hex'));
+ return crypto.createHash('sha256').update(secret).digest('hex');

The fix replaces the misused createCipher call with a proper crypto.createHash('sha256') pipeline:

  1. .update(secret) — feeds the actual secret value into the hash function.
  2. .digest('hex') — finalises the hash and returns a hex-encoded string.

This produces a deterministic, well-defined 64-character hex string derived from secret, which is the intended behavior. It also removes the dependency on the deprecated (and in newer Node versions, absent) Blowfish cipher path.

Before vs. After — side by side:

// BEFORE
var createHash = function(secret) {
    var cipher = crypto.createCipher('blowfish', secret);
    return(cipher.final('hex'));          // secret is never fed in; output is meaningless
};

// AFTER
var createHash = function(secret) {
    return crypto.createHash('sha256').update(secret).digest('hex');  // correct
};

Prevention & Best Practices

1. Apply Authentication Middleware at the Router Level

Rather than adding IP checks inside individual route handlers, consider an Express middleware that guards an entire router:

function localhostOnly(req, res, next) {
    const ip = req.ip || req.connection.remoteAddress;
    const loopback = ['127.0.0.1', '::1', '::ffff:127.0.0.1'];
    if (!loopback.includes(ip)) {
        return res.status(403).send('Forbidden');
    }
    next();
}

app.get('/token', localhostOnly, tokenHandler);

This pattern is easier to audit and harder to accidentally bypass than per-route checks scattered through the codebase.

2. Never Expose Administrative Endpoints Without Authentication

Any endpoint that generates credentials, tokens, or secrets should require proof of identity before responding. The minimum viable controls are:

  • Localhost binding: bind the server to 127.0.0.1 rather than 0.0.0.0 if remote access is not required.
  • IP allowlist: as implemented in this fix.
  • API key or bearer token: for endpoints that legitimately need to be reachable remotely.

3. Use Correct Cryptographic APIs

Node.js's crypto module distinguishes clearly between ciphers (encrypt/decrypt) and hashes (one-way digest). Using a cipher where a hash is needed — especially without calling .update() — produces silent, incorrect output. Always:

  • Use crypto.createHash() for one-way token fingerprinting.
  • Use crypto.createHmac() when the output needs to be keyed (MAC).
  • Avoid deprecated functions (createCipher, createDecipher) entirely.

4. Lint for Deprecated Crypto Calls

Add node/no-deprecated-api (from eslint-plugin-node) to your ESLint config to catch calls to deprecated Node.js APIs at development time:

{
  "plugins": ["node"],
  "rules": {
    "node/no-deprecated-api": "error"
  }
}

5. Relevant Standards

  • OWASP API Security Top 10: API2:2023 — Broken Authentication
  • CWE-306 — Missing Authentication for Critical Function
  • CWE-326 — Inadequate Encryption Strength (the Blowfish/createCipher issue)
  • OWASP Authentication Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html

Key Takeaways

  • The /token route in plugin/multiplex/index.js had zero authentication — any caller on the network received a valid presenter token. Always treat token-generation endpoints as privileged operations requiring identity verification.
  • crypto.createCipher('blowfish', secret) without .update() produces output that does not depend on secret — a subtle bug that would survive code review unless a reviewer knew to look for the missing .update() call.
  • Localhost IP checks must cover all three loopback representations (127.0.0.1, ::1, ::ffff:127.0.0.1) to be effective in dual-stack Node.js environments.
  • Library vulnerabilities have a multiplier effect — every downstream project that embeds this plugin inherits the unauthenticated endpoint. Fixing it upstream protects the entire ecosystem.
  • Deprecated Node.js crypto APIs are not just a style issuecrypto.createCipher was removed in Node 22; code relying on it will throw at runtime on modern runtimes, making the cryptographic weakness a reliability issue as well.

How Orbis AppSec Detected This

  • Source: Inbound HTTP GET /token request — specifically req.ip / req.connection.remoteAddress — from any arbitrary network client.
  • Sink: The unconditional res.send(createHash(secret)) call at line 50 of plugin/multiplex/index.js, which returned a presenter token to any caller without an intervening identity check.
  • Missing control: No authentication middleware, no IP restriction, and no session or API-key validation existed on the /token route prior to this fix.
  • CWE: CWE-306 — Missing Authentication for Critical Function (secondary: CWE-326 — Inadequate Encryption Strength for the createCipher misuse).
  • Fix: An IP allowlist block was inserted at the top of the /token handler to return HTTP 403 for all non-loopback callers, and createCipher was replaced with createHash('sha256').

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

An unauthenticated token endpoint is one of the most direct paths to privilege escalation an attacker can find: no exploitation chain, no memory corruption, no race condition — just an HTTP GET and a credential lands in their terminal. The reveal.js multiplex plugin's /token route was exactly that: a door left open because the server was assumed to be local-only, but never enforced to be local-only.

The fix is compact — eleven lines changed — but it closes two distinct weaknesses simultaneously: the missing access control and the broken cryptographic primitive. Both changes are instructive reminders that security assumptions must be expressed in code, not documentation, and that the Node.js crypto module requires careful API selection to produce meaningful output.

When building or auditing Express servers that manage tokens or credentials, always ask: "What stops an unauthenticated caller from reaching this handler?" If the answer is "nothing," the handler needs a guard.


References

Frequently Asked Questions

What is a missing authentication vulnerability?

A missing authentication vulnerability occurs when a sensitive function or endpoint—such as one that generates privileged tokens or controls system state—can be invoked by any caller without supplying credentials or proving identity.

How do you prevent unauthenticated API endpoints in Node.js Express?

Apply an authentication middleware (or at minimum an IP allowlist for internal-only routes) before any route handler that produces privileged output. For public-facing services, use session tokens, API keys, or OAuth; for localhost-only admin routes, verify `req.ip` against the loopback address before processing the request.

What CWE is missing authentication for a critical function?

CWE-306 — "Missing Authentication for Critical Function." Related identifiers include CWE-862 (Missing Authorization) and OWASP API Security Top 10: API2:2023 Broken Authentication.

Is running the server on a non-standard port enough to prevent this vulnerability?

No. Security through obscurity does not constitute authentication. Any attacker who discovers the port—through scanning, log leakage, or documentation—can exploit an unauthenticated endpoint regardless of the port number.

Can static analysis detect missing authentication in Express routes?

Yes. Tools such as Semgrep, ESLint security plugins, and AI-assisted scanners like Orbis AppSec can flag Express route handlers that lack authentication middleware or perform sensitive operations without an identity check.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #24

Related Articles

critical

How Missing Authentication Middleware Happens in Node.js APIs and How to Fix It

A critical vulnerability in a Node.js Panel Connector API (CVE-2025-7783) left 14 endpoints—including shell command execution, file deletion, and file writing—completely open to unauthenticated access. The comment in the source code even declared "NO AUTH — Full Open Access," making it a textbook example of a missing authentication control. The fix adds a Bearer token middleware guard on all `/api` routes, blocking unauthorized requests before they reach any sensitive handler.

critical

How OAuth CSRF Attacks Happen in Node.js and How to Fix Them

A missing OAuth state parameter validation in `src/account_manager.js` left the `startOAuthServer()` function vulnerable to CSRF attacks, allowing an attacker to inject their own authorization code into a victim's active OAuth session. The fix generates a cryptographically random state token using `crypto.randomBytes()`, returns it alongside the server handle, and rejects any callback where the returned state doesn't match — closing the attack window entirely. This affects all downstream consume

critical

How Unauthenticated API Endpoint Exposure happens in Node.js and how to fix it

A critical vulnerability in `api/firebase-config.js` exposed all Firebase configuration values — including API keys, app IDs, and project IDs — to any unauthenticated caller. With no access controls, CORS restrictions, or rate limiting in place, attackers could retrieve live credentials and directly access Firebase services. The fix adds shared-secret authentication using timing-safe comparison, origin validation, and method enforcement.

high

How Middleware and Proxy Bypass happens in Next.js App Router and how to fix it

CVE-2026-64642 is a high-severity authentication bypass vulnerability in Next.js that affects App Router applications using Turbopack with a single locale configuration. The flaw allows attackers to circumvent middleware and proxy security controls, potentially gaining unauthorized access to protected routes. Upgrading from Next.js 16.2.7 to 16.2.11 closes the vulnerability entirely.

critical

How OAuth 2.0 CSRF happens in PHP and how to fix it

A critical OAuth 2.0 CSRF vulnerability in `login_weibo.php` allowed attackers to forge Weibo login requests by exploiting the missing `state` parameter validation. Without this check, an attacker could trick a victim's browser into completing an OAuth flow with the attacker's authorization code, potentially hijacking the victim's session. The fix generates a cryptographically random state token, stores it in the session, and validates it on callback.

critical

How eval() Code Injection happens in JavaScript and how to fix it

A critical code injection vulnerability was discovered in `js/lib/jsencrypt.js` at line 195, where a direct `eval()` call executed a JavaScript string shim for the `process` object in browser environments. If an attacker could influence the string passed to `eval()`—through a compromised dependency, a man-in-the-middle attack, or supply chain tampering—they could achieve arbitrary JavaScript execution in any user's browser. The fix replaces the `eval()` call with the equivalent inline JavaScript