Skip to content

Commit f9eba3d

Browse files
committed
client: Do not allow PLAIN on insecure connection
Also add connection.isSecure() method Fixes #1040
1 parent c5b54c8 commit f9eba3d

File tree

11 files changed

+107
-20
lines changed

11 files changed

+107
-20
lines changed

packages/client-core/lib/Client.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class Client extends Connection {
4747
// after the confidentiality and integrity of the stream are protected via TLS
4848
// or an equivalent security layer.
4949
// https://xmpp.org/rfcs/rfc6120.html#rfc.section.4.7.1
50-
const from = this.socket?.isSecure() && this.jid?.bare().toString();
50+
const from = this.isSecure() && this.jid?.bare().toString();
5151
if (from) headerElement.attrs.from = from;
5252
return this.Transport.prototype.header(headerElement, ...args);
5353
}

packages/client-core/test/Client.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,20 @@ test("header", () => {
3232

3333
const entity = new Client();
3434
entity.Transport = Transport;
35-
entity.socket = {};
3635

3736
entity.jid = null;
38-
entity.socket.isSecure = () => false;
37+
entity.isSecure = () => false;
3938
expect(entity.header(<foo />)).toEqual(<foo />);
4039

4140
entity.jid = null;
42-
entity.socket.isSecure = () => true;
41+
entity.isSecure = () => true;
4342
expect(entity.header(<foo />)).toEqual(<foo />);
4443

4544
entity.jid = new JID("foo@bar/example");
46-
entity.socket.isSecure = () => false;
45+
entity.isSecure = () => false;
4746
expect(entity.header(<foo />)).toEqual(<foo />);
4847

4948
entity.jid = new JID("foo@bar/example");
50-
entity.socket.isSecure = () => true;
49+
entity.isSecure = () => true;
5150
expect(entity.header(<foo />)).toEqual(<foo from="foo@bar" />);
5251
});

packages/client/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,21 @@ Returns a promise that resolves once all the stanzas have been sent.
265265

266266
If you need to send a stanza to multiple recipients we recommend using [Extended Stanza Addressing](https://xmpp.org/extensions/xep-0033.html) instead.
267267

268+
### isSecure
269+
270+
Returns whether the connection is considered secured.
271+
272+
```js
273+
console.log(xmpp.isSecure());
274+
```
275+
276+
Considered secure:
277+
278+
- localhost, 127.0.0.1, ::1
279+
- encrypted channels (wss, xmpps, starttls)
280+
281+
This method returns false if there is no connection.
282+
268283
### xmpp.reconnect
269284

270285
See [@xmpp/reconnect](/packages/reconnect).
Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
11
const ANONYMOUS = "ANONYMOUS";
2+
const PLAIN = "PLAIN";
23

34
export default function createOnAuthenticate(credentials, userAgent) {
4-
return async function onAuthenticate(authenticate, mechanisms, fast) {
5+
return async function onAuthenticate(authenticate, mechanisms, fast, entity) {
56
if (typeof credentials === "function") {
67
await credentials(authenticate, mechanisms, fast);
78
return;
89
}
910

10-
if (
11-
!credentials?.username &&
12-
!credentials?.password &&
13-
mechanisms.includes(ANONYMOUS)
14-
) {
15-
await authenticate(credentials, ANONYMOUS, userAgent);
16-
return;
17-
}
11+
credentials.token ??= await fast?.fetch();
1812

19-
credentials.token = await fast?.fetch?.();
20-
21-
await authenticate(credentials, mechanisms[0], userAgent);
13+
const mechanism = getMechanism({ mechanisms, entity, credentials });
14+
await authenticate(credentials, mechanism, userAgent);
2215
};
2316
}
17+
18+
export function getMechanism({ mechanisms, entity, credentials }) {
19+
if (
20+
!credentials?.username &&
21+
!credentials?.password &&
22+
!credentials?.token &&
23+
mechanisms.includes(ANONYMOUS)
24+
) {
25+
return ANONYMOUS;
26+
}
27+
28+
if (entity.isSecure()) return mechanisms[0];
29+
30+
return mechanisms.find((mechanism) => mechanism !== PLAIN);
31+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { getMechanism } from "../lib/createOnAuthenticate.js";
2+
3+
it("returns ANONYMOUS if available and there are no credentials", () => {
4+
expect(
5+
getMechanism({
6+
credentials: {},
7+
mechanisms: ["PLAIN", "ANONYMOUS"],
8+
}),
9+
).toBe("ANONYMOUS");
10+
});
11+
12+
it("returns the first mechanism if the connection is secure", () => {
13+
expect(
14+
getMechanism({
15+
credentials: { username: "foo", password: "bar" },
16+
mechanisms: ["PLAIN", "SCRAM-SHA-1"],
17+
entity: { isSecure: () => true },
18+
}),
19+
).toBe("PLAIN");
20+
});
21+
22+
it("does not return PLAIN if the connection is not secure", () => {
23+
expect(
24+
getMechanism({
25+
credentials: { username: "foo", password: "bar" },
26+
mechanisms: ["PLAIN", "SCRAM-SHA-1"],
27+
entity: { isSecure: () => false },
28+
}),
29+
).toBe("SCRAM-SHA-1");
30+
31+
expect(
32+
getMechanism({
33+
credentials: { username: "foo", password: "bar" },
34+
mechanisms: ["PLAIN"],
35+
entity: { isSecure: () => false },
36+
}),
37+
).toBe(undefined);
38+
});

packages/connection/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ class Connection extends EventEmitter {
2222
this.root = null;
2323
}
2424

25+
isSecure() {
26+
return !!this.socket?.isSecure();
27+
}
28+
2529
async _streamError(condition, children) {
2630
try {
2731
await this.send(
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Connection from "../index.js";
2+
3+
test("isSecure()", () => {
4+
const conn = new Connection();
5+
6+
conn.socket = null;
7+
expect(conn.isSecure()).toBe(false);
8+
9+
conn.socket = { isSecure: () => false };
10+
expect(conn.isSecure()).toBe(false);
11+
12+
conn.socket = { isSecure: () => true };
13+
expect(conn.isSecure()).toBe(true);
14+
});

packages/sasl/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export default function sasl({ streamFeatures, saslFactory }, onAuthenticate) {
8484
});
8585
}
8686

87-
await onAuthenticate(done, mechanisms);
87+
await onAuthenticate(done, mechanisms, null, entity);
8888

8989
await entity.restart();
9090
});

packages/sasl2/index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,12 @@ export default function sasl2({ streamFeatures, saslFactory }, onAuthenticate) {
106106
throw new SASLError("SASL: No compatible mechanism available.");
107107
}
108108

109-
await onAuthenticate(done, mechanisms, fast_available && fast);
109+
await onAuthenticate(
110+
done,
111+
mechanisms,
112+
fast_available ? fast : null,
113+
entity,
114+
);
110115

111116
async function done(credentials, mechanism, userAgent) {
112117
if (fast_available) {

packages/test/mockSocket.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ class MockSocket extends net.Socket {
66
cb?.();
77
});
88
}
9+
isSecure() {
10+
return true;
11+
}
912
}
1013

1114
export default function mockSocket() {

0 commit comments

Comments
 (0)