Skip to content

Commit e23df01

Browse files
authored
Merge commit from fork
1 parent 3f2e023 commit e23df01

3 files changed

Lines changed: 152 additions & 1 deletion

File tree

‎ChangeLog.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
Version 0.19-SNAPSHOT
2+
[fix] Check Last Will topic for writability rights before publish.
23
[fix] Reject a malformed $share subscription before parsing it (remote DoS). (#958)
34
[fix] Keep the session event loop alive when a command throws (remote DoS). (#957)
45
[fix] Guard the periodic expiration task so one failing entity can't disable all expirations. (#959)

‎broker/src/main/java/io/moquette/broker/PostOffice.java‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,10 @@ private void publishWill(ISessionsRepository.Will will) {
336336
}
337337
}
338338
MqttPublishMessage willPublishMessage = publishBuilder.build();
339-
339+
if (!authorizator.canWrite(Topic.asTopic(will.topic), "", WILL_PUBLISHER)) {
340+
LOG.error("client is not authorized to publish Last Will Testament on topic: {}", will.topic);
341+
return;
342+
}
340343
publish2Subscribers(WILL_PUBLISHER, messageExpiryInstant, willPublishMessage);
341344
}
342345

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Copyright (c) 2012-2026 The original author or authors
3+
* ------------------------------------------------------
4+
* All rights reserved. This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License v1.0
6+
* and Apache License v2.0 which accompanies this distribution.
7+
*
8+
* The Eclipse Public License is available at
9+
* http://www.eclipse.org/legal/epl-v10.html
10+
*
11+
* The Apache License v2.0 is available at
12+
* http://www.opensource.org/licenses/apache2.0.php
13+
*
14+
* You may elect to redistribute this code under either of these licenses.
15+
*/
16+
17+
package io.moquette.integration;
18+
19+
import io.moquette.broker.Server;
20+
import io.moquette.broker.config.IConfig;
21+
import io.moquette.broker.config.MemoryConfig;
22+
import io.moquette.broker.security.AcceptAllAuthenticator;
23+
import io.moquette.broker.security.IAuthorizatorPolicy;
24+
import io.moquette.broker.subscriptions.Topic;
25+
import io.moquette.testclient.Client;
26+
import org.awaitility.Awaitility;
27+
import org.awaitility.Durations;
28+
import org.eclipse.paho.client.mqttv3.IMqttClient;
29+
import org.eclipse.paho.client.mqttv3.MqttClient;
30+
import org.eclipse.paho.client.mqttv3.MqttClientPersistence;
31+
import org.eclipse.paho.client.mqttv3.MqttException;
32+
import org.eclipse.paho.client.mqttv3.persist.MqttDefaultFilePersistence;
33+
import org.junit.jupiter.api.AfterEach;
34+
import org.junit.jupiter.api.BeforeAll;
35+
import org.junit.jupiter.api.BeforeEach;
36+
import org.junit.jupiter.api.Test;
37+
import org.junit.jupiter.api.io.TempDir;
38+
import org.slf4j.Logger;
39+
import org.slf4j.LoggerFactory;
40+
41+
import java.io.IOException;
42+
import java.nio.file.Path;
43+
import java.util.Properties;
44+
45+
import static io.moquette.broker.ConnectionTestUtils.EMPTY_OBSERVERS;
46+
47+
/**
48+
* Regression test for GHSA-9jjc-fw8x-fmwx: an anonymous client with no write
49+
* permission on a topic must not be able to deliver a Will message to subscribers
50+
* of that topic on abrupt disconnection.
51+
*/
52+
public class WillUnauthorizedPublishTest {
53+
54+
private static final Logger LOG = LoggerFactory.getLogger(WillUnauthorizedPublishTest.class);
55+
56+
private static final String RESTRICTED_TOPIC = "restricted/topic";
57+
58+
private Server server;
59+
private IConfig config;
60+
61+
// Paho subscriber: has read access, must NOT receive the offender's will
62+
private IMqttClient subscriber;
63+
private MessageCollector messageCollector;
64+
65+
// Low-level client: anonymous, no write access, sets a Will on restricted/topic
66+
private Client offendingClient;
67+
68+
@TempDir
69+
Path tempFolder;
70+
71+
@BeforeAll
72+
public static void beforeTests() {
73+
Awaitility.setDefaultTimeout(Durations.TWO_SECONDS);
74+
}
75+
76+
@BeforeEach
77+
public void setUp() throws Exception {
78+
String dbPath = IntegrationUtils.tempH2Path(tempFolder);
79+
startServer(dbPath);
80+
81+
MqttClientPersistence dataStore = new MqttDefaultFilePersistence(
82+
IntegrationUtils.newFolder(tempFolder, "subscriber").getAbsolutePath());
83+
subscriber = new MqttClient("tcp://localhost:1883", "ValidSubscriber", dataStore);
84+
messageCollector = new MessageCollector();
85+
subscriber.setCallback(messageCollector);
86+
87+
offendingClient = new Client("localhost");
88+
}
89+
90+
protected void startServer(String dbPath) throws IOException {
91+
server = new Server();
92+
final Properties configProps = IntegrationUtils.prepareTestProperties(dbPath);
93+
config = new MemoryConfig(configProps);
94+
95+
// ACL: only read is permitted on restricted/topic; write is denied for everyone
96+
final IAuthorizatorPolicy aclPolicy = new IAuthorizatorPolicy() {
97+
@Override
98+
public boolean canWrite(Topic topic, String user, String client) {
99+
return !RESTRICTED_TOPIC.equals(topic.toString());
100+
}
101+
102+
@Override
103+
public boolean canRead(Topic topic, String user, String client) {
104+
return true;
105+
}
106+
};
107+
108+
server.startServer(config, EMPTY_OBSERVERS, null, new AcceptAllAuthenticator(), aclPolicy);
109+
}
110+
111+
@AfterEach
112+
public void tearDown() throws Exception {
113+
offendingClient.close();
114+
try {
115+
if (subscriber.isConnected()) {
116+
subscriber.disconnect();
117+
}
118+
} catch (MqttException ex) {
119+
LOG.warn("subscriber disconnect error", ex);
120+
}
121+
server.stopServer();
122+
}
123+
124+
@Test
125+
public void willMessageMustNotBeDeliveredWhenPublisherLacksWritePermission() throws Exception {
126+
LOG.info("*** willMessageMustNotBeDeliveredWhenPublisherLacksWritePermission ***");
127+
128+
// Step 1 – valid subscriber connects and subscribes to the restricted topic
129+
subscriber.connect();
130+
subscriber.subscribe(RESTRICTED_TOPIC, 0);
131+
132+
// Step 2 – anonymous offending client connects with a Will targeting the restricted topic
133+
offendingClient
134+
.clientId("OffendingClient")
135+
.connect(RESTRICTED_TOPIC, "unauthorized will payload");
136+
137+
// Step 3 – offending client disconnects abruptly, which triggers the Will
138+
offendingClient.close();
139+
140+
// Step 4 – assert the subscriber does NOT receive the will message.
141+
// The Will write must be subject to the same ACL check as a normal publish.
142+
Awaitility.await("Will message must NOT be delivered when writer lacks write permission")
143+
.during(Durations.ONE_SECOND)
144+
.atMost(Durations.TWO_SECONDS)
145+
.until(() -> !messageCollector.isMessageReceived());
146+
}
147+
}

0 commit comments

Comments
 (0)