Vulnerabilities
The Password Check Had Not Finished, but the Door Was Already Open: Rocket.Chat CVE-2026-28514
CVE-2026-28514 affected Rocket.Chat's Enterprise microservice password flow: an unresolved bcrypt Promise was treated as truthy before its false result arrived, after which the service issued a session token and attached the impersonated identity to the DDP connection.

In this article
1 At two in the morning, a wrong password returned a valid pass
Begin with an ordinary-looking event on a quiet operations shift. A Rocket.Chat WebSocket connection opens, invokes login, and succeeds. It does not hammer the service with thousands of guesses or force an expensive hash-cracking campaign. It supplies an existing username and one incorrect password. The connection receives a user identifier, a login token, and an expiration time. Moments later it can behave as the named account and ask for the channels that account may enter.
The unsettling part is that bcrypt never approved the password. The comparison function will eventually produce false, exactly as it should. The caller makes its decision before that answer arrives. It receives the Promise that represents future completion, treats the Promise object as a true value, skips the rejection branch, and continues into session creation. The cryptography remains intact while the program mistakes unfinished work for proof.
Rocket.Chat tracks the issue as CVE-2026-28514 and GHSA-w6vw-mrgv-69vf, rates it Critical, and maps it to CWE-287. The project advisory says an unauthenticated attacker can use any arbitrary password for a user who has a password set when the username is known or guessable.
1.1 A successful-login event did not prove that the password ever passed
Authentication is usually explained as a short ordered exchange. The service locates an account, compares the supplied credential, branches on a Boolean result, and creates a session only after success. That order gives familiar telemetry its meaning: rejections reflect failed proof, while success implies that proof completed. CVE-2026-28514 breaks the order without breaking the shape of the success response. A durable session appears even though the decisive Boolean was never consumed.
The possibility of one clean-looking success changes the investigator’s first question. A dashboard showing no increase in failed logins is weak reassurance because the vulnerable flow may record a single success. Useful questions start with the entry point and state change: Which architecture handled the connection? Which image digest was running? Did the target have a local bcrypt field? Was a new resume session appended? What subscriptions, reads, messages, or administrative methods followed from that session?
The effect also survives the first socket. Once the false check is skipped, the code creates a raw stamped token, stores its hashed form in the user document, and returns credentials that support continued session use. Disconnecting the initial WebSocket does not erase that stored identity. Actual lifetime follows the workspace's login-expiration configuration, so response must distinguish stopping new bypass attempts from invalidating sessions already issued by an affected build.
Severity numbers are useful when kept beside scope. NVD publishes a CVSS 3.1 base score of 9.8, while the CNA publishes CVSS 4.0 at 9.3. Both describe network reachability, no prior privilege, no separate approving user, and high potential impact. The public vulnerable route belongs to the Enterprise microservice implementation, however. An inventory has to establish deployment architecture before applying the version matrix to every Rocket.Chat server bearing the same product name.
The account condition is equally concrete. The affected function queries by username and projects services.password.bcrypt. A nonexistent user returns false before session work. An account without a local bcrypt value does not place a Promise into the dangerous expression. The advisory's wording—any user with a password set—captures this constraint. Purely external-identity accounts may follow other paths that require their own deployment-specific review.
Known or guessable usernames do not require a password leak. An address convention, public profile, invitation, support screenshot, archived channel reference, or predictable service-account name may reveal the identifier. A single privileged account that retains a local password and has a discoverable username is enough to make the path consequential. Large-scale enumeration is not a prerequisite for the mechanism described in the project advisory.
Account lockout and rate limiting still have defensive value, yet neither repairs this decision. A one-attempt success can occur before a failure threshold accumulates. Restricting network sources or temporarily disabling local login for especially sensitive accounts can reduce immediate opportunity during an upgrade. The durable correction is to ensure that the password result itself participates in authentication before any session side effect.
Defenders can model each login as three separately observable facts. First, the request claims an identity. Second, the credential check resolves to a successful primitive result. Third, the system creates an authenticated session. Normal code connects them in that order. The affected code permits the third fact while the second is absent. Recording these facts independently exposes a class of defects that a simple success-versus-failure counter can conceal.
The public record does not name compromised workspaces or provide a universal network indicator. It establishes mechanism, affected branches, prerequisites, and the resulting authenticated state. That is sufficient for urgent remediation. A claim that a particular organization experienced takeover still belongs to its connection, session, and post-login evidence. Preserving that distinction makes the report more useful, not less serious.
1.2 One missing wait inherited every permission of the named account
“A missing await” sounds almost too small for a critical advisory. Its position explains the rating. Every operation below the check assumes that identity has already been proven: token generation, database persistence, DDP connection state, subscriptions, and methods. The source mistake occupies one expression, while the trust it releases travels through the rest of the account's authority.
Impact therefore follows the impersonated user. A regular member may see only joined rooms and direct conversations. A support lead, compliance reviewer, integration bot, room owner, or administrator may reach far more. The project advisory describes possible account takeover; once a session is established as the target account, its available DDP methods, channels, and messages remain bounded by that account’s actual workspace permissions. Local event evidence must establish whether any particular workspace was read or changed.
Exposure and incident status therefore need different gates. A matching Enterprise microservice architecture and affected release require immediate patching. They do not prove that someone used the defect. An unfamiliar session tied to the affected path, followed by activity inconsistent with the user, supports a compromise finding. Conversely, a quiet failed-login graph cannot close the case because explicit rejection may never occur.
The same technical advisory discusses a separate username-query issue in the account service under another CVE. The two findings can amplify each other, since one can weaken username knowledge while this one weakens password proof. This report keeps CVE-2026-28514 precise: a known or guessable username and a local password hash are enough for its published model. Evidence for the adjacent issue should be tracked separately.
For an operations team, the first protected artifact is session lineage. Capture workspace, running digest, user ID, connection ID, source after trusted-proxy processing, client characteristics, success time, and immediate actions. Do not paste raw login tokens into an incident ticket. A raw token is a credential. A hash fingerprint and its creation metadata are usually enough to correlate server, database, and device records.
For a development team, the first protected artifact is the actual type. A variable named valid is not necessarily a Boolean. The called function explicitly returns Promise<boolean>, and the logical expression preserves that Promise. Review must follow the return type into the security condition and confirm that an asynchronous result has been resolved, rejected safely, and narrowed to the primitive value the branch expects.
The opening scene leaves two facts on the desk. Rocket.Chat genuinely issued a reusable session, and the supplied password was never accepted by the comparison result. The next chapter follows the request through ddp-streamer, the account service, and MongoDB to show how that impossible combination became ordinary state that every downstream component trusted.
2 One WebSocket login crossed four rooms that trusted one another
Rocket.Chat's Enterprise microservice model separates responsibilities that a monolithic process would keep together. The official architecture guide assigns WebSocket and DDP handling to ddp-streamer, account management and login authentication to the accounts service, authorization to its own service, presence to another, and inter-service messaging to NATS. The split permits each pressure point to scale independently. It also means that one login conclusion travels between components as a trusted fact.
The published route begins at the workspace's /websocket endpoint. ddp-streamer registers a DDP method named login, accepts resume, user, and password fields, and calls Account.login(). This front service does not compare the password itself. It awaits a service result. A false result produces a 403; a result object causes the connection to adopt the returned identity and session.
2.1 The front desk asked once while the account service made two different choices
Account.login() first chooses the authentication mode. A supplied resume token goes to loginViaResume(). If no resume token is selected and both user and password are present, the function calls loginViaUsername(). Otherwise it returns false. CVE-2026-28514 resides in the username-and-password branch. The dispatch decision itself does not silently convert an invalid resume token into a password success.
The username function performs the next choice. It queries for a user whose username matches and projects only the local bcrypt field needed for comparison. If there is no record, it returns false before token work begins. A successful query is merely account discovery. It does not authenticate the requester, and it should leave the code poised to decide on the supplied credential.
The dangerous choice follows. The function assigns the logical-AND expression containing validatePassword() to valid, then asks if (!valid). With a bcrypt hash present, the value is the Promise object. The function therefore declines to take the false-return branch and enters the exact code reserved for a proved password. The second choice is where a found account becomes an authenticated identity without its proof.
Once the result object returns, ddp-streamer writes three pieces of connection state. this.userId receives the target user identifier. this.userToken receives the hashed token, and the underlying connection's loginToken receives the same hashed value. The service emits logged-in events and sends the raw token, expiration time, user identifier, and password authentication type back to the client.
Every room in this route appears to honor its contract. The WebSocket method awaits the account service. The account service returns an object shaped like ILoginResult. The session helper persists the token. The DDP server converts that object into live connection state. No downstream component has a reason to recompute bcrypt. The contract violation is concentrated inside the account service, but its output is authoritative everywhere else.
Microservices did not create the language error, and scaling is not the root cause. Distribution changes where evidence lives. The reverse proxy may know only that a WebSocket upgrade occurred. ddp-streamer knows connection and login events. The account service knows which authentication branch ran. MongoDB records a new session hash. Post-login methods and subscriptions may be observed in other components. Looking at a single container can miss the sequence.
A useful telemetry design carries stable context through those rooms: workspace, request or connection ID, resolved user ID, service instance, source image digest, result category, and timestamp. Passwords and raw tokens do not belong in that context. The final Boolean can be represented as a sanitized outcome, and a session write can emit only a nonreversible identifier and event metadata. The goal is correlation without creating a second credential store in logs.
Other login surfaces may exist in a deployment. The public advisory establishes the Enterprise DDP microservice route, so a passing REST login test cannot sign off the WebSocket route. An operator can inspect whether enabled HTTP paths share the same account service and test them as adjacent coverage. Conclusions should name the transport and backend actually exercised, keeping a good result on one path from hiding an old component on another.
2.2 Version numbers matter only after the Enterprise service path is proved
The project advisory consistently names the Enterprise Edition ddp-streamer and the account service it uses. The relevant files live under ee/apps. Rocket.Chat's architecture documentation distinguishes a monolithic process from containers dedicated to accounts, WebSocket/DDP, authorization, and presence. A responsible inventory does not project this one implementation across every installation merely because all instances display the Rocket.Chat brand.
For the officially supported Kubernetes and Helm model, start with rendered manifests and running Pods, not a values file that may be stale. Record microservices.enabled, account and ddp-streamer replicas, container image tags and immutable digests, namespace, ingress host, and WebSocket route. For a vendor-operated workspace, request the corresponding architecture and fixed-version attestation from the service provider.
A tag can lie by accident. An internal image called 7.13.3 may have been built from an earlier tree, and a chart value that omits the patch number may conceal the actual container. A digest tied to source provenance provides the stronger fact. Inspecting the running package for the awaited expression and performing the wrong-password acceptance test connect that provenance to behavior.
Large organizations often run mixed topologies. A central workspace may use microservices while a smaller regulated enclave remains monolithic. A disaster-recovery cluster may lag several weeks. A staging environment may still contain an 8.0 release candidate. Treat each workspace as an independent row so a repaired primary cluster does not hide an accessible legacy route and an out-of-scope monolith does not flood the urgent list.
Network placement changes priority, not code semantics. An internal-only ingress reduces exposure to the public internet but remains reachable from compromised endpoints, VPN users, or adjacent systems. A generic WAF sees a structurally valid DDP login and cannot reliably determine that the password will compare false. Authentication must fail inside the component that owns the password result.
Next add account conditions without exporting password hashes. For privileged and sensitive identities, record whether a local bcrypt field exists, whether the account is active, the role and business owner, last expected activity, and count of current sessions. A workspace that intends to use SSO exclusively may discover old local-password state during this review. That state is relevant even if nobody remembers using it recently.
Architecture, version, and account state together create an actionable exposure result. An affected build with no reachable Enterprise route still requires upgrade, while its immediate investigation priority can be lower. A reachable microservice path with affected accounts and a privileged local-password identity deserves preservation, patching, and session review first. Each priority then traces to observable deployment facts.
The deployment map leads us to the room where the decision fails: an asynchronous function whose return type is explicit and a condition that asks an object to stand in for its answer. The next chapter takes that expression apart under ECMAScript rules, showing why the behavior is deterministic and why common operational mitigations cannot change it.
3 The Promise told the truth; the condition questioned the wrong value
The vulnerable line fits on a sticky note. validatePassword() calls bcrypt.compare() and explicitly returns Promise<boolean>. The username login function confirms that a hash exists, assigns the call result to valid, and immediately evaluates if (!valid). The intention reads clearly. The runtime semantics omit a necessary transition from “future answer” to “answer.”
Bcrypt comparison is asynchronous. Calling it produces a Promise that represents eventual completion. That Promise is a container, not the Boolean inside it. A wrong password eventually fulfills the container with false; a correct password fulfills it with true. Without waiting, the condition sees the same category in both cases: an object that has already been created.
3.1 Logical AND preserved the object, and logical NOT stamped it as acceptable
The affected expression begins with the optional bcrypt value and uses logical AND before validatePassword(). In JavaScript, && does not guarantee a Boolean output. If the first operand is truthy, the operator returns the second operand itself. With a stored hash present, valid therefore becomes the Promise returned by the comparison function.
The following logical NOT invokes Boolean conversion. ECMAScript's ToBoolean operation returns false for a small group of primitive values such as undefined, null, numeric zero, NaN, and an empty string. Ordinary objects fall through to true. A Promise is an object. Its Boolean value does not change when its internal state moves from pending to fulfilled, and it does not borrow the truth value of its fulfillment result.
The failure is deterministic, not a narrow timing contest. Even if bcrypt completes quickly, the function call returns a Promise to the current expression. JavaScript does not automatically extract a fulfilled value just because work has already finished. Without await or an equivalent continuation, the next operator receives the object. The code has already skipped rejection by the time the comparison's false value becomes available.
The type-level result rules out several tempting mitigations. Adding CPU, reducing connection concurrency, changing bcrypt cost, or restarting the Pod does not make the object false. The issue is not a high-load race and does not depend on a particular scheduler. Once the branch reaches an existing user with a local hash, language semantics reliably favor the success path in the affected source.
There may be no rejected Promise to alert on. A wrong password is a normal comparison outcome, so bcrypt can fulfill with false. The program ignores that legitimate answer. Monitoring that looks only for unhandled rejections, exception stacks, event-loop failures, or process restarts can remain quiet while an authentication decision has already gone wrong.
Plain TypeScript compilation may accept a Promise in a truthy position, so type-aware lint has to cover the business decision that the compiler permits. The typescript-eslint no-misused-promises rule targets Promise values placed in conditions, while no-floating-promises covers returned Promises that are abandoned along with their errors or sequencing. Authentication packages benefit from both rules with type information enabled.
An explicit Boolean conversion is not a repair. Boolean(promise) or a double negation makes the same object truthiness more obvious and still returns true. The code has to await the Promise, handle rejection safely, inspect the primitive Boolean, and only then create identity. Review should ask where the value came from, not merely whether the syntax looks Boolean.
3.2 One wait restored the order in which identity may be created
The upstream correction waits inside the expression: the hash must exist and the resolved password result must be true. When the password is wrong, await validatePassword(...) yields false, if (!valid) executes, and the function exits before token generation. A correct password yields true and proceeds through the existing success code. The patch changes the gate, not the session format.
The durable requirement is larger than the token await. Credential proof must finish before any identity-bearing state is created. That requirement belongs in tests and review criteria, since future refactors may rename the helper or replace bcrypt. Every asynchronous credential, account-status, second-factor, risk, or policy check must resolve and fail closed before the first privileged side effect.
Other waits already surround the vulnerable line, which helps explain how the omission blends in. ddp-streamer awaits Account.login(). The username function awaits the database query and later awaits saveSession(). An async function does not implicitly wait for every nested Promise. Each call site still chooses among awaiting, returning, aggregating, or intentionally discarding its result.
Reviewers can work backward from the first irreversible security action. Here it is creating and persisting a new login token. Above that action, user existence, account eligibility, and password correctness must all be settled primitive values. If any remains a Promise, callback handle, partially populated response, or unchecked error object, the proof sequence is incomplete.
A focused regression matrix covers at least five cases: nonexistent username, existing user without a local hash, existing user with a wrong password, existing user with a correct password, and a valid resume token. The first three must not create a password session. The fourth must create one that works. The fifth preserves the independent resume flow. A deliberately removed wait should make the security test fail, proving that the suite actually covers this cause.
End-to-end acceptance must traverse DDP. A unit test can establish loginViaUsername() behavior but cannot alone prove that the public method handles every result safely. Use a dedicated account in an isolated workspace, submit a known wrong password through the normal WebSocket route, and expect rejection. Then test the correct password and controlled resume token. Record outcomes and session counts without retaining credentials.
Exception behavior belongs in the matrix. If bcrypt throws or the Promise rejects, authentication must fail through the established error handling and must not produce a temporary session in a catch block. Waiting allows that failure to enter normal async control flow. Test database timeout and service failure as adjacent conditions so partial results never become success.
Precise types make these controls effective. Keep Promise<boolean>, false | ILoginResult, and concrete credential shapes instead of widening security code to any. A type system cannot prove the entire authentication design, but it can give lint the evidence needed to reject a Promise in a conditional and make review deviations visible.
The line now has an unambiguous story. Bcrypt reaches the right answer. The Promise faithfully carries it. The program fails because it decides before opening that container. The next chapter follows the premature success after it leaves the condition, showing how a truthy object becomes database state, a live user identity, and access governed by the victim's role.
4 Once the bad decision landed, it became a real reusable identity
If the defect only made a local variable truthy for one stack frame, its effect would vanish when the function returned. Rocket.Chat's success branch deliberately creates more durable state. It calls _generateStampedLoginToken(), derives a hashed stamped token, awaits saveSession(), and returns a complete login result. Those are the correct actions after a valid password and dangerous actions after an unresolved one.
saveSession() is concise. It updates the user by ID and pushes the new hashed token into services.resume.loginTokens. The raw value goes back toward the client while the database retains the form used to recognize a later session. The storage design avoids keeping immediately usable plaintext tokens in the user document, yet it also means the mistaken authentication persists beyond the originating request.
4.1 One raw token left traces in the database, the service, and the connection
The result returned by the username function includes user ID, raw token, hashed token, expiration, and authentication type password. This is a full login artifact, not a tentative “password seems acceptable” flag. ddp-streamer assigns the user and token to its own connection state, places the hashed token on the underlying connection, emits logged-in events, and returns the raw token metadata to the client.
Each state offers a different investigative view. The client or device inventory may retain the usable session. The user document shows a resume-token hash was added. ddp-streamer can show a connection becoming a specific user. Later methods and subscriptions show what that identity attempted. Any one source may be absent because of retention, privacy controls, or version-specific logging. Several sources aligned in time can support a stronger conclusion.
Raw tokens should not be collected casually. Copying one to a ticket, chat room, or general SIEM makes a credential available to more systems and people. Preserve creation time, user, device or session identifier, nonreversible fingerprint, source context, first and last activity, and revocation status. If handling the raw value is unavoidable, isolate it under credential controls and invalidate it as soon as evidence requirements allow.
A database change helps recover history after ingress logs expire, but it is not proof of exploitation by itself. Every legitimate password login can append a resume session. Join the write time with source address, running build, DDP success, client characteristics, and subsequent behavior. A user who installed a new official client may produce the same storage event with an innocent explanation.
A WebSocket upgrade is also incomplete evidence. HTTP 101 only establishes the transport before authentication. High-value records are a successful DDP login response, assignment of user ID, logged-in events, a matching session write, and authorized calls that follow. When a proxy cannot inspect DDP messages, application telemetry becomes the authoritative layer for the identity transition.
Session lifetime is configuration-sensitive. The Account class has an initial login-expiration value and also loads Accounts_LoginExpiration from settings while listening for changes. An investigation window should use the workspace setting history and observed session metadata. A token created before patching can remain relevant long after the last vulnerable Pod has exited.
Patch deployment and session cleanup consequently belong to separate work items. The new code prevents additional any-password sessions. Revocation removes identities already issued. Password recovery addresses credentials or trust that may have changed during the incident. Activity review determines what an impersonated account reached. Closing the first item does not close the other three.
4.2 The world after login unfolded under the victim's existing role
Once the connection has a userId, available DDP methods and subscriptions are evaluated as that account. CVE-2026-28514 does not grant one identical administrator capability to every attacker. It grants the named identity. A regular member, private-room owner, support agent, compliance account, integration bot, and system administrator expose very different consequences.
The public controlled validation shows that an authenticated session could interact with available DDP methods, connect to channels, and receive messages. This demonstrates a meaningful state change. The public report cannot list each organization's rooms, role definitions, applications, or data. Workspace owners need to reconstruct reach from role assignments, room membership, application permissions, and the actions recorded for the suspicious session.
High value is broader than an administrator label. A monitoring bot may read operational alerts. A legal or compliance identity may search retained history. A support supervisor may see customer conversations. An integration user may hold permissions connected to webhooks or external systems. Prioritize accounts by what an impersonated session can do, then use role names as one input.
End-to-end encrypted rooms require care in impact statements. Server-side account authentication does not automatically give every client the keys needed to decrypt all historical ciphertext. Device key state and Rocket.Chat's encryption design remain separate. Do not transform a proved account session into a universal decryption claim. Review room type, subscription, client key state, and actual access events.
Integrity impact follows permitted operations. An attacker may send messages as the user, alter profile settings, join eligible rooms, or call administrative methods when the role allows them. Determine which occurred from message creation, method calls, role and setting changes, and audit records. At the moment only a session is established, report unauthorized authentication state and update the impact as evidence accumulates.
Availability is also action-dependent. Product CVSS expresses potential technical impact, not a statement that a particular workspace lost service. An internal report can say that a privileged session was confirmed while no outage evidence was found. Keeping product scoring beside local facts prevents a severe general rating from becoming invented local damage.
Time correlation turns a session into a behavior story. Starting at creation, examine the first minutes and the token's entire active period for room subscriptions, history reads, messages, file access, settings changes, device registration, and API activity. If the same token appears from multiple origins or client types, preserve every occurrence. The first connection may not contain the most sensitive action.
At this point the two-o'clock event has a complete consequence: the password check did not pass, the program created a session anyway, the DDP connection inherited a real user identity, and normal authorization opened whatever that identity already owned. The next chapter moves into repository history to identify the causal repair and the fixed floor for each maintained release branch.
5 The repair was one wait, distributed across seven release lines
The project advisory links affected source at revision 05c415b9…, where the dangerous expression remains visible. Repository history identifies dd7032a0a4c25b6be5b3af05c83f8d07d5102424 as the commit that adds the wait. Its message says ddp streamer was not waiting for some request completion, and its author date is January 12, 2026. The authentication source change is one line.
A one-line causal repair still has to reach every maintained product line. Rocket.Chat lists fixed floors for seven branches: 7.8.6, 7.9.8, 7.10.7, 7.11.4, 7.12.4, 7.13.3, and 8.0.0. A 7.10 workspace compares itself with 7.10.7; it does not use 7.13.3 as a numeric threshold. Asset logic has to understand branch membership and prerelease versions.
5.1 The source diff restored order, and provenance proves that diff reached a running artifact
The fixed expression waits for validatePassword() before the logical expression settles. A wrong password produces false, and the next line exits. A correct password produces true, and the unchanged token path continues. The small source modification limits collateral behavior while creating a crisp acceptance criterion: no session side effect can occur after a false comparison.
The same upstream commit includes a package changeset and CI workflow changes. Causality belongs to the one-line authentication diff; unrelated files in the commit should not all be described as parts of the password fix. The 7.13.3 release tag points to revision 1864c260…, whose username login file contains the awaited expression. The 8.0.0 release page records the major release on January 12.
Four evidence layers answer different questions: the vulnerable snapshot shows the defect, the repair commit shows the causal change, a fixed release shows what users could deploy, and immutable build provenance shows what the cluster actually runs. Preserve the vendor advisory, fixed source or release tag, internal build record, and image digest as a chain. Calling every layer a generic “patch link,” or retaining only a screenshot, loses reproducibility after private rebuilds, tag overwrites, or cached layers.
For a downstream fork, text search is an initial check, not the full conclusion. Finding await validatePassword is encouraging. A wrapper may have renamed the function, changed its return type, or moved token creation. Confirm that the final condition holds a primitive Boolean and that every failure exits before token generation, persistence, identity binding, or events.
The build pipeline should retain source commit, dependency lock, compiler configuration, and artifact digest. Bcrypt version is not the root cause, so a dependency upgrade cannot substitute for the application change. Conversely, a chart may advertise a fixed Rocket.Chat version while an Enterprise account-service image remains old. Microservice releases require per-component provenance.
Rolling deployment can temporarily mix safe and unsafe Pods. If service discovery still routes account requests to an old replica, some logins remain exposed. Wait for desired, updated, and available replica counts to converge, verify that previous ReplicaSets have no serving Pods, and inspect every running digest. Remove the vulnerable image from automatic rollback defaults so an unrelated outage does not silently restore it later.
Fix availability predates full public detail: the repair reached a release before the project advisory on March 5 and the technical publication on March 12. That coordinated sequence gave operators a fixed build before the mechanism was widely described. Historical review still begins when an affected instance became reachable, not when the advisory became public.
Subsequent supported releases should inherit the fix, but the minimum fixed floor is not a permanent destination. Upgrade to a currently supported maintenance version after compatibility review so later security and reliability changes are included. If a constraint forces temporary residence on a minimum branch, assign an owner and an exit date.
5.2 Seven fixed floors must become seven explicit inventory rules
The practical comparison is branch-specific. Releases in 7.8 before 7.8.6 are affected. Releases in 7.9 before 7.9.8 are affected. The corresponding floors are 7.10.7, 7.11.4, 7.12.4, and 7.13.3. For the 8.0 line, publicly identified release candidates preceding the 8.0.0 final release remain on the affected side, while 8.0.0 is the listed fixed start.
| Release line | First listed fixed version | Example match | Acceptance focus |
|---|---|---|---|
| 7.8 | 7.8.6 | 7.8.5 and earlier | Account-service source contains the resolved Boolean check |
| 7.9 | 7.9.8 | 7.9.7 and earlier | No old Pod remains in service discovery |
| 7.10 | 7.10.7 | 7.10.6 and earlier | Wrong password cannot create a session |
| 7.11 | 7.11.4 | 7.11.3 and earlier | Correct password and resume flow still work |
| 7.12 | 7.12.4 | 7.12.3 and earlier | Digest maps to reviewed source provenance |
| 7.13 | 7.13.3 | 7.13.2 and earlier | Prioritize replacement of 7.13.2 and earlier on this line |
| 8.0 | 8.0.0 | rc0 through rc5 | Complete major-release migration prerequisites |
Do not compare these as plain strings. Lexical sorting can place 7.9.10 below 7.9.8, and a naive parser may rank 8.0.0-rc5 incorrectly against the final release. Use a semantic-version implementation that understands numeric components and prerelease precedence, then apply the correct branch rule.
A version at or above the listed floor still needs validation. Private builds can derive from older source, a registry can serve a stale layer, and a rollout can fail. A downstream fork may have backported the fix before a vendor floor, but remove such an exception from the vulnerable list only after fixed source, build provenance, and runtime behavior agree.
Rocket.Chat 8.0.0 is a major release with database and deprecation considerations described in its release notes. Security urgency shortens the planning window but does not erase backups, compatibility testing, or upgrade prerequisites. A workspace on a supported 7.x branch can first take its fixed patch release while a major-version program proceeds safely.
Unsupported legacy lines require a supported target, not an indefinite exception based on the table. During a constrained transition, reduce reachable sources, disable unnecessary high-privilege local passwords, and increase session monitoring. These temporary measures reduce opportunity; none changes the Promise truthiness in the old code.
Record rollout start and completion precisely. Investigators need the last time an old Pod could accept a login. Align that moment with session creation, ingress connections, deployment events, and Pod termination. A note saying “upgraded Tuesday” cannot distinguish exposure before, during, and after a rolling change.
The seven branches converge on one behavioral fact: a wrong password must be rejected and must not append a resume session. Version selection identifies the code expected to contain that behavior. Runtime acceptance proves the deployed code actually has it. The next chapter carries those facts into a Kubernetes change and verifies each serving replica.
6 Bringing the patch into a cluster means proving every door received the new lock
Remediation begins with the fixed-version matrix and continues after an image pull completes. Rocket.Chat's official guide supports microservices on Kubernetes with its Helm chart and allows accounts, ddp-streamer, authorization, presence, and NATS to scale independently. Any vulnerable account-service replica that can still receive authentication traffic leaves inconsistent behavior inside an otherwise upgraded workspace.
A change owner must control configuration and observed state. Preserve Helm values, rendered manifests, current digests, replica topology, and ingress routing before deployment. Roll out a vendor-fixed release. Wait for old replicas to leave. Then send acceptance tests through the real external route. If a result fails, this record separates a wrong version, stale image, partial rollout, unexpected route, and test-account mistake.
6.1 Start with immutable digests and follow service discovery to the serving Pods
The first inventory table has one row per workspace: ingress host, namespace, Helm release, chart version, Rocket.Chat application version, account-service digest, ddp-streamer digest, replica counts, database cluster, and NATS endpoint. Keep image tags for readability while using digests for decisions. Read running data from the Kubernetes API so the table reflects the cluster, not only intended configuration in Git.
The second table follows traffic. Which gateway sends /websocket to which Service? Which labels select the ddp-streamer Pods? How does ddp-streamer reach the account service? Are there canary Services, regional clusters, disaster-recovery routes, or service meshes that can deliver a small percentage to a legacy revision? Exposure follows actual discovery and routing.
The third table adds minimum account metadata for privileged or sensitive identities: local password present, active state, role, business owner, expected last login, and current device count. Do not export bcrypt hashes. A simple presence flag is sufficient for the condition described by the advisory. If policy expects SSO-only accounts, lingering local password state becomes a separate cleanup task.
Preserve rollback configuration while removing an unsafe image from the default rollback destination. If the new release produces an operational fault, restore a build that still contains the security repair or deploy a reviewed backport. Disaster-recovery manifests, offline registries, and automated “last known good” jobs must receive the same rule; otherwise a future outage can reintroduce the old login path.
Temporary controls can reduce noise and exposure during change. Restrict management-account use, narrow WebSocket sources, or disable a high-risk local account when business owners approve. Every temporary measure needs a start, end, owner, and recovery condition. It must not become an undocumented network exception after the application is safe.
Capture Pod start times, node, and digest as change evidence. A Deployment pointing at a new tag does not prove that every container restarted, particularly with mutable tags, image caches, and pull policies. If the organization uses signatures or software attestations, record verification and map it to the fixed source commit. This lowers uncertainty between registry and runtime.
The application fix does not call for hand-editing MongoDB. Session revocation is incident response and should use supported Rocket.Chat controls so related state remains consistent and auditable. Ad hoc database scripts can remove too much, leave caches stale, or destroy the very metadata needed to understand when a session appeared.
6.2 A small hard acceptance suite guards failure, success, and resume login
Create a dedicated test user with a known local password and access only to a harmless test room. Record and clear unrelated sessions before the run so a new entry is obvious. Send every case through the same ingress and WebSocket endpoint used by clients, covering the gateway, ddp-streamer, account service, and database.
- Wrong password. Supply the existing username and a known wrong password. Expect DDP login rejection. Also assert that
services.resume.loginTokensdoes not grow, the connection never receives the user ID, no logged-in event is emitted, and no authenticated subscription succeeds. A client error paired with any of those side effects is a failed security acceptance. - Correct password. Supply the correct password. It should succeed and create exactly one expected session, proving that the update did not disable password authentication. Record time, digest, and sanitized result, then log out the test session through supported controls. Do not place its returned raw token in the change ticket.
- Valid resume token. Use a controlled valid resume token and expect normal resume behavior.
- Invalid or revoked resume token. Expect rejection. The CVE repair is in the username branch, but release acceptance should ensure that the adjacent authentication mode still behaves correctly after the broader version update.
- Nonexistent user or no local hash. Cover both nonexistent-user and existing-user-without-local-hash cases. Both should reject and create no session. Public responses should avoid revealing which account condition failed. Restricted internal telemetry can retain a reason code for diagnosis without exposing account enumeration details to the client.
Run the matrix against every serving digest. If the load balancer cannot target a replica directly, use an auditable temporary test route, controlled per-Pod forwarding, or repeated scheduling with verified instance telemetry. Remove any test route afterward. Production users should never become the probabilistic probe that discovers one old Pod.
A negative control makes the test credible. In an isolated, disposable replica of the affected build, the same wrong-password case should demonstrate the unsafe session side effect; in the fixed replica it should reject. Keep this laboratory away from production identities and networks, destroy created sessions, and do not preserve a vulnerable service as a long-running convenience.
Exercise failure modes as well. If bcrypt rejects, MongoDB becomes unavailable, or the service call times out, authentication must fail safely without producing partial identity. These cases extend beyond the missing wait but enforce the same invariant: no unproved or incomplete result may reach token creation.
Performance and monitoring results show whether the change is healthy; they do not replace security acceptance. A wrong password should add a rejection without adding a session, while a correct password should produce both success and the expected session write. Latency and throughput can expose deployment trouble, but cannot prove that a wrong password was rejected.
The final sign-off package should be replayable: fixed source or release, running digests, test-account conditions, entry point, timestamps, expected and observed outcomes, session counts, and replica coverage. Successful and failed cases should also join through one correlation ID across the proxy, ddp-streamer, account service, and session record, with aligned clocks and no password or token leakage.
Remove test sessions, temporary routes, diagnostic verbosity, and extra grants, then have a second engineer review the cleanup. The rollback candidate must pass the same wrong-password case; otherwise it cannot become the stable, disaster-recovery, or emergency rollback build. A test entry point or unproved rollback artifact can reopen the door that the upgrade just closed.
When every replica rejects the wrong password with no session side effect, the new lock is installed. The historical record does not disappear with it. The next chapter starts from pre-patch sessions and behavior to distinguish ordinary login, automatic reconnects, and genuinely suspicious identity takeover.
7 The investigation becomes clear only after the “successful login”
Deployment answers whether the flaw can occur now. Historical investigation asks whether it occurred before the repair. CVE-2026-28514 complicates the second question because the result resembles a normal success. One success event has ordinary syntax, and one new resume token is normal product behavior. Investigative value appears when independent records meet on the same user, session, connection, and time.
Define the window before writing queries. Start when the workspace first ran an affected Enterprise microservice build with a reachable path, or at the earliest retained deployment history if precision is unavailable. Continue through the final old replica, successful wrong-password rejection tests, and suspicious-session revocation. The advisory publication date is not a natural beginning for local exposure.
7.1 Five evidence segments together begin to resemble unauthorized authentication
The first segment is entry context: source address after trusted-proxy handling, WebSocket path, connection time, user agent, gateway, and request rate. Ingress logs usually cannot see a password decision and should never record the password. They establish who opened a transport from where, but they do not establish the DDP login result.
The second segment is application authentication: connection ID, claimed username or resolved user ID, account-service branch, success or rejection, service instance, and build digest. If the deployed version lacks structured fields, use existing logs and audit facilities without enabling payload-rich debug output in production. Future telemetry can add the minimum event schema under restricted access.
The third segment is session state: when a resume-token hash appeared, which device or client it represents, last activity, and revocation. Obtain database snapshots or change audit through the organization's evidence process. A full hash need not spread across systems. A nonreversible fingerprint, array change, and time can supply correlation while protecting session material.
The fourth segment is authenticated behavior: room subscriptions, history reads, message and file activity, settings changes, role operations, application calls, and administration methods. Compare the new session with the user's ordinary client and schedule. A privileged identity that immediately touches many private rooms from a new origin deserves a different response from a known device reconnecting after an upgrade.
The fifth segment is user and endpoint confirmation. Ask whether the account owner changed devices, used a VPN, upgraded a client, or worked from a new location. Compare endpoint security records, browser sessions, and enterprise identity events. User recollection removes many benign changes, but it does not replace server evidence. An uncertain answer on a sensitive account keeps the technical review open.
Together these segments form an evidence ladder. A WebSocket 101 alone is weak. A matching WebSocket, DDP success, and new session are stronger. Add an unfamiliar source and sensitive behavior, and the sequence can support unauthorized use. If only the vulnerable version is known and historical telemetry is gone, report that use cannot be excluded; do not convert missing records into a clean finding.
Sequence-based detection fits this vulnerability better than a failed-login threshold. One useful pattern is: unfamiliar source opens a connection; a privileged user obtains a new password session; the same connection quickly joins multiple private rooms; then it reads history or invokes a sensitive method. A single event in that chain has benign explanations, while the joined state changes are much harder to dismiss.
Clocks can corrupt the apparent order. Normalize UTC, preserve original zones, and document known drift among gateway, Kubernetes node, application, and database time. Use connection ID, user ID, and session creation to align records when timestamps differ by seconds. Do not draw a more precise chronology than the source systems support.
A transparent scoring model can help triage. A vulnerable digest, new origin, fresh session, privileged account, and sensitive follow-on activity increase confidence. A user-confirmed device change, known corporate egress, and planned client rollout may reduce it. Keep each factor beside its raw evidence so an analyst can review the score instead of accepting an opaque number.
Logging itself needs a credential-safety check. If an application or gateway currently records full DDP parameters, determine whether passwords or tokens entered those logs and treat them as sensitive stores. A long-term event schema uses an allowlist: method, outcome, user, connection, service revision, and correlation ID. Investigating one credential flaw must not create another exposure.
7.2 When detailed logs are gone, session inventory and behavior can narrow the unknown
Many workspaces do not retain months of DDP events. Begin with current active sessions and available token metadata: creation or first-seen time, last activity, device, source, and user confirmation. Preserve the minimum metadata for unfamiliar long-lived sessions, revoke them, and record the revocation result. Current state cannot reconstruct every historical action, but it can stop an identity that remains usable.
Rocket.Chat's account settings documentation provides a user-visible device list with client, operating system, recent login activity, and device ID, along with remote logout. Users can also log out other logged-in locations. Administrators should follow the workspace's permissions and audit process when acting for another user. Asking a high-risk account owner to review this list is a fast way to surface an obviously unfamiliar client.
Longer-retained proxy records can be worked backward from a session's first activity. Even without DDP payloads, source, connection time, route, and duration provide context. Shared NAT, enterprise gateways, mobile networks, and privacy relays mean that an IP is not a person. Use address consistency as one factor, never the sole attribution.
Message and administration audit can also lead back to the session. An unexpected settings change or message supplies a user and time range, which can then be joined to connection and device records. For end-to-end encrypted rooms, preserve server-visible metadata and follow legal and privacy controls; do not attempt to defeat encryption to fill an investigative gap.
When token origin cannot be determined, containment can follow account sensitivity and session age. An unexplained active session on an administrator or integration identity warrants prompt invalidation. Regular users can be forced through reauthentication in a managed window if broad revocation is needed. Disruption is usually smaller than leaving an unknown authenticated identity active, but the incident owner should approve scope.
Do not reproduce arbitrary-password login against real production accounts. Public evidence establishes the mechanism, and a production test would create another unauthorized session and contaminate records. Dynamic validation belongs in an isolated copy with a dedicated account, limited data, written cleanup, and no path to external systems. Production review remains version, state, and historical evidence collection.
Track adjacent anomalies separately. The same technical publication describes another CVE affecting username-query input. Structured username values or query errors in telemetry should become their own timeline and response item. A combined-impact claim requires evidence for both mechanisms; it should not be smuggled into CVE-2026-28514 solely because the findings share a service.
CISA's public SSVC entry recorded exploitation as none on March 10, 2026. That describes the public knowledge available at that time. It is not forensic clearance for a private workspace. Cite it as context while completing local review. The absence of publicly known exploitation neither repairs a reachable Critical flaw nor decides what happened inside one organization's logs.
Close the investigation with one of three candid outcomes. First, an unauthorized session and its actions are confirmed. Second, a high-confidence suspicious sequence exists but full impact is unproven. Third, no related activity is found within available records, with residual uncertainty caused by retention. Each conclusion names examined sources, missing sources, and controls already applied.
Once connection, session, device, and behavior finally align, the quiet successful login can be understood for what it was. The last chapter safely removes that borrowed identity, restores accounts and services, and turns the lesson into compiler, lint, test, release, and production controls that make a future Promise much harder to mistake for proof.
8 Revoke the false identity, then make the next Promise impossible to misread
Response has to preserve evidence and stop ongoing risk at the same time. Capture minimum deployment, connection, session, and behavior metadata. Move every authentication request onto a fixed replica. Invalidate confirmed or unexplained sessions. Reset affected local passwords and review external identity state. Then determine which rooms, messages, files, applications, and administrative actions were reachable while the borrowed identity remained valid.
A password change alone is incomplete containment. The vulnerable path issues a resume session whose later use follows Rocket.Chat's session controls. The product provides device review, device logout, and logout from other locations; administrative action for another user follows local permission and audit procedures. Every revocation should produce a timestamped result that can be joined back to the incident.
8.1 Recover the workspace by taking back identity before measuring what it did
When bypass is confirmed, freeze logs and relevant database metadata before retention or remediation overwrites them. Record the current session inventory, then revoke active sessions for the affected identity. If abuse appears ongoing, temporarily disable the account or restrict WebSocket access. Each emergency action needs an approver, start time, owner, and restoration condition.
Reset the local password and inspect SSO links, API tokens, personal access tokens, applications, and bot credentials associated with the account. This CVE does not prove those secrets leaked. If the authenticated session could create, view, or exercise them, use observed behavior and logging quality to choose rotation scope. Administrators, integrations, and compliance accounts come first.
Assess content and configuration according to the victim's permissions and actual events. Enumerate rooms joined or accessed, history reads, downloaded files, sent or edited messages, settings changes, role operations, and external calls. Give business owners confirmed facts and residual unknowns. Do not copy the product's maximum possible impact into a workspace report without matching evidence.
User communication should be actionable: unusual time and device, sessions revoked, need to sign in again, password or second-factor steps, and messages to report. Exclude other users' data, internal addresses, and token material. If regulated information may be involved, route notification and reporting decisions through legal, privacy, and compliance owners.
After service restoration, continue monitoring across a period that includes client reconnects and expected session expiration. Watch for reuse of revoked sessions, one source shifting among usernames, an old digest reappearing, the account returning from an unfamiliar device, or a post-fix wrong-password test succeeding. Include serving revision in each alert so a rollback is visible immediately.
Run a blameless engineering review. The useful question is not who forgot one keyword. Ask why return types, type-aware lint, unit tests, DDP acceptance, maintenance-branch release, and production verification allowed a Promise into the decision. Durable actions include rejecting Promise conditionals, asserting zero session side effects on failure, and requiring the security matrix for account-service changes.
Use the lint rule that matches the misuse. @typescript-eslint/no-misused-promises with conditional checks is designed to reject Promise values in branches. no-floating-promises catches calls that are left unhandled. Both need type information. Authentication packages should not disable type-aware analysis as a convenient response to build time or noisy legacy code; exceptions need narrow scope and review.
Tests should observe ordering through side-effect probes. When comparison returns false, token generation, saveSession(), identity assignment, and logged-in events all have zero calls. When it returns true, they occur in the expected order. This is stronger than checking only the returned false value, because a future refactor could write a session before returning failure.
8.2 Return to two in the morning, where the repaired door waits for the answer
Replay the opening with the same dedicated test account, the same wrong password, and the same WebSocket route. ddp-streamer forwards the request. The account service finds the local hash. validatePassword() returns a Promise. This time the code stops and waits. Bcrypt resolves to false, the username function returns failure, and the DDP method rejects login.
The absent effects matter as much as the rejection. The token generator does not run. The user document receives no resume session. The connection never adopts the user ID. No logged-in event is emitted. Room subscriptions do not inherit the target's permissions. Acceptance should make each absence observable instead of trusting a single client error string.
Now supply the correct password. Comparison resolves to true and the intended path continues: create a token, store its hash, bind the connection, and return a session. The repair does not disable password login or shut down microservices. It makes success correspond to the credential result again, which is the behavior users and downstream services always assumed.
CVE-2026-28514 shows how normal an asynchronous security failure can look. The Promise need not reject. Bcrypt need not throw. The service need not crash. The response can be well formed and the database consistent. Following state transitions reveals the missing proof that an error dashboard will not.
A maintainable engineering rule fits in one sentence: every durable action that creates identity or authority must occur after all relevant asynchronous checks resolve successfully. Use explicit primitive results. Test that failure leaves no token, session, event, or downstream call. Reject Promise values in conditions at review and CI time.
The operational rule is equally concise: a version and architecture match trigger remediation, while state evidence decides the incident. Enterprise service topology, release line, digest, and local-password condition establish exposure. Connections, session writes, devices, and behavior establish use. Where retention is weak, state the unknown and reduce it through revocation.
The public record did not report known exploitation in the wild, and CISA's March 10 SSVC record used exploitation none. That constrains public claims but does not lower patch priority. The route is unauthenticated, attack complexity is low, and success yields a real account session. A local workspace still has to examine its own evidence.
For an owner beginning late, the shortest responsible path is concrete: prove whether accounts and ddp-streamer microservices are serving; upgrade each branch to its fixed floor or a later supported release; verify every digest rejects a wrong password without a session; inventory privileged local-password accounts; correlate historical sessions; and revoke unexplained identity.
For a maintainer, the lasting artifacts are also concrete: exact return types, type-aware rules, zero-side-effect failure tests, a DDP acceptance case, synchronized maintenance branches, source-to-image provenance, and per-replica production sign-off. Those controls make the next asynchronous refactor answerable before it ships.
The two-o'clock connection finally ends where it should. The wrong password still makes bcrypt return false, and the program finally waits to hear it. There is no token, no new resume session, and no borrowed identity. The door closes through a simple restored order that every authentication system depends on: establish proof first, issue passage second.
Research record
9Evidence, objects, and sources
The material below preserves the identifiers and references used in this report.
9.1Research objects
Products, actors, techniques, affected objects, and control points discussed in the report.
Rocket.Chat Enterprise ddp-streamer password authentication bypass
Rocket.Chat project security advisory
Reachable DDP login method
Enterprise microservice account authentication component
Upstream commit that awaits password comparison
Project-advised fixed floor for each release line
9.2Event chronology
- Vulnerabilities reported privately
The authentication findings were submitted through the project's private vulnerability-reporting channel.
- Repair commit and 8.0.0
Upstream added the wait, and the public disclosure timeline records the initial repair in version 8.0.0.
- Version 7.13.3 released
The fixed 7.13.3 source contains the awaited password decision.
- Project advisory published
Rocket.Chat published GHSA-w6vw-mrgv-69vf with fixed floors for seven release lines.
- Technical advisory published
The coordinated analysis of GHSL-2026-004 and the adjacent account-service issue was published.
- SOSEC source review completed
SOSEC reconciled public advisories, pinned source, repair commits, release floors, detection conditions, and response guidance.
9.3Sources and material
- Rocket.Chat GHSA-w6vw-mrgv-69vf project advisoryhttps://github.com/RocketChat/Rocket.Chat/security/advisories/GHSA-w6vw-mrgv-69vf
- GitHub Security Lab Rocket.Chat technical advisoryhttps://securitylab.github.com/advisories/GHSL-2026-004_GHSL-2026-005_Rocket_Chat/
- CVE Program record for CVE-2026-28514https://www.cve.org/CVERecord?id=CVE-2026-28514
- NVD version, CVSS, and SSVC recordhttps://nvd.nist.gov/vuln/detail/CVE-2026-28514
- Rocket.Chat commit that awaits password validationhttps://github.com/RocketChat/Rocket.Chat/commit/dd7032a0a4c25b6be5b3af05c83f8d07d5102424
- Affected snapshot password validation helperhttps://github.com/RocketChat/Rocket.Chat/blob/05c415b94cb91907de39a39c6d277579258f334e/ee/apps/account-service/src/lib/utils.ts
- Affected snapshot username login functionhttps://github.com/RocketChat/Rocket.Chat/blob/05c415b94cb91907de39a39c6d277579258f334e/ee/apps/account-service/src/lib/loginViaUsername.ts
- Affected snapshot account login dispatcherhttps://github.com/RocketChat/Rocket.Chat/blob/05c415b94cb91907de39a39c6d277579258f334e/ee/apps/account-service/src/Account.ts
- Affected snapshot session persistence helperhttps://github.com/RocketChat/Rocket.Chat/blob/05c415b94cb91907de39a39c6d277579258f334e/ee/apps/account-service/src/lib/saveSession.ts
- Affected snapshot DDP login and connection statehttps://github.com/RocketChat/Rocket.Chat/blob/05c415b94cb91907de39a39c6d277579258f334e/ee/apps/ddp-streamer/src/configureServer.ts
- Rocket.Chat 7.13.3 fixed username login functionhttps://github.com/RocketChat/Rocket.Chat/blob/7.13.3/ee/apps/account-service/src/lib/loginViaUsername.ts
- Rocket.Chat 7.13.3 releasehttps://github.com/RocketChat/Rocket.Chat/releases/tag/7.13.3
- Rocket.Chat 8.0.0 releasehttps://github.com/RocketChat/Rocket.Chat/releases/tag/8.0.0
- Rocket.Chat microservice architecture and scaling guidehttps://docs.rocket.chat/docs/microservices
- Rocket.Chat device and other-login management guidehttps://docs.rocket.chat/docs/manage-your-account-settings
- ECMAScript ToBoolean specificationhttps://tc39.es/ecma262/multipage/abstract-operations.html#sec-toboolean
- typescript-eslint no-misused-promises rulehttps://typescript-eslint.io/rules/no-misused-promises/
- typescript-eslint no-floating-promises rulehttps://typescript-eslint.io/rules/no-floating-promises/
- MITRE CWE-287 Improper Authenticationhttps://cwe.mitre.org/data/definitions/287.html