Skip to content

Commit dbb3e9f

Browse files
committed
Issue #540 - Better validation of application requests to prevent protocol errors
Signed-off-by: James Sutton <james.sutton@uk.ibm.com>
1 parent c7e3dfb commit dbb3e9f

7 files changed

Lines changed: 62 additions & 48 deletions

File tree

org.eclipse.paho.client.mqttv3/src/main/java/org/eclipse/paho/client/mqttv3/MqttAsyncClient.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,8 +1063,10 @@ public IMqttToken subscribe(String[] topicFilters, int[] qos, Object userContext
10631063
throw new IllegalArgumentException();
10641064
}
10651065

1066-
// remove any message handlers for individual topics
1066+
// remove any message handlers for individual topics and validate topicFilter
10671067
for (int i = 0; i < topicFilters.length; ++i) {
1068+
// Check if the topic filter is valid before subscribing
1069+
MqttTopic.validate(topicFilters[i], true/* allow wildcards */);
10681070
this.comms.removeMessageListener(topicFilters[i]);
10691071
}
10701072

@@ -1077,8 +1079,7 @@ public IMqttToken subscribe(String[] topicFilters, int[] qos, Object userContext
10771079
}
10781080
subs.append("topic=").append(topicFilters[i]).append(" qos=").append(qos[i]);
10791081

1080-
// Check if the topic filter is valid before subscribing
1081-
MqttTopic.validate(topicFilters[i], true/* allow wildcards */);
1082+
10821083
}
10831084
// @TRACE 106=Subscribe topicFilter={0} userContext={1} callback={2}
10841085
log.fine(CLASS_NAME, methodName, "106", new Object[] { subs.toString(), userContext, callback });

org.eclipse.paho.mqttv5.client/src/main/java/org/eclipse/paho/mqttv5/client/MqttAsyncClient.java

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,13 @@ public class MqttAsyncClient implements MqttClientInterface, IMqttAsyncClient {
253253
// second
254254
private boolean reconnecting = false;
255255
private static Object clientLock = new Object(); // Simple lock
256-
private MqttSessionState mqttSession = new MqttSessionState(); // Variables that exist within the life of an MQTT session
257-
private MqttConnectionState mqttConnection = new MqttConnectionState(); // Variables that exist within the life of an MQTT connection.
256+
257+
// Variables that exist within the life of an MQTT session
258+
private MqttSessionState mqttSession = new MqttSessionState();
259+
260+
// Variables that exist within the life of an MQTT connection.
261+
private MqttConnectionState mqttConnection = new MqttConnectionState();
262+
258263
private ScheduledExecutorService executorService;
259264
private MqttPingSender pingSender;
260265

@@ -424,9 +429,9 @@ public MqttAsyncClient(String serverURI, String clientId) throws MqttException {
424429
* {@link MqttClientPersistence} interface. An implementer of this interface
425430
* that safely stores messages must be specified in order for delivery of
426431
* messages to be reliable. In addition
427-
* {@link MqttConnectionOptions#setCleanStart(boolean)} must be set to false.
428-
* In the event that only QoS 0 messages are sent or received or cleanStart is
429-
* set to true then a safe store is not needed.
432+
* {@link MqttConnectionOptions#setCleanStart(boolean)} must be set to false. In
433+
* the event that only QoS 0 messages are sent or received or cleanStart is set
434+
* to true then a safe store is not needed.
430435
* </p>
431436
* <p>
432437
* An implementation of file-based persistence is provided in class
@@ -527,9 +532,9 @@ public MqttAsyncClient(String serverURI, String clientId, MqttClientPersistence
527532
* {@link MqttClientPersistence} interface. An implementer of this interface
528533
* that safely stores messages must be specified in order for delivery of
529534
* messages to be reliable. In addition
530-
* {@link MqttConnectionOptions#setCleanStart(boolean)} must be set to false.
531-
* In the event that only QoS 0 messages are sent or received or cleanStart is
532-
* set to true then a safe store is not needed.
535+
* {@link MqttConnectionOptions#setCleanStart(boolean)} must be set to false. In
536+
* the event that only QoS 0 messages are sent or received or cleanStart is set
537+
* to true then a safe store is not needed.
533538
* </p>
534539
* <p>
535540
* An implementation of file-based persistence is provided in class
@@ -575,14 +580,13 @@ public MqttAsyncClient(String serverURI, String clientId, MqttClientPersistence
575580
DataOutputStream dos = new DataOutputStream(baos);
576581
MqttDataTypes.encodeUTF8(dos, clientId);
577582
// Remove the two size bytes.
578-
if(dos.size() - 2 > 65535) {
583+
if (dos.size() - 2 > 65535) {
579584
throw new IllegalArgumentException("ClientId longer than 65535 characters");
580585
}
581-
586+
582587
} else {
583588
clientId = "";
584589
}
585-
586590

587591
MqttConnectionOptions.validateURI(serverURI);
588592

@@ -608,7 +612,8 @@ public MqttAsyncClient(String serverURI, String clientId, MqttClientPersistence
608612
log.fine(CLASS_NAME, methodName, "101", new Object[] { clientId, serverURI, persistence });
609613

610614
this.persistence.open(clientId);
611-
this.comms = new ClientComms(this, this.persistence, this.pingSender, this.executorService, this.mqttSession, this.mqttConnection);
615+
this.comms = new ClientComms(this, this.persistence, this.pingSender, this.executorService, this.mqttSession,
616+
this.mqttConnection);
612617
this.persistence.close();
613618
this.topics = new Hashtable<String, MqttTopic>();
614619

@@ -899,7 +904,7 @@ public IMqttToken connect(MqttConnectionOptions options, Object userContext, Mqt
899904
userToken, userContext, callback, reconnecting, mqttSession, mqttConnection);
900905
userToken.setActionCallback(connectActionListener);
901906
userToken.setUserContext(this);
902-
907+
903908
this.mqttConnection.setSendReasonMessages(this.connOpts.isSendReasonMessages());
904909

905910
// If we are using the MqttCallbackExtended, set it on the
@@ -1134,7 +1139,7 @@ public String getCurrentServerURI() {
11341139
* if the topic contains a '+' or '#' wildcard character.
11351140
*/
11361141
protected MqttTopic getTopic(String topic) {
1137-
MqttTopicValidator.validate(topic, false/* wildcards NOT allowed */);
1142+
MqttTopicValidator.validate(topic, false/* wildcards NOT allowed */, true);
11381143

11391144
MqttTopic result = (MqttTopic) topics.get(topic);
11401145
if (result == null) {
@@ -1200,7 +1205,7 @@ public IMqttToken subscribe(String topicFilter, int qos) throws MqttException {
12001205
return this.subscribe(new MqttSubscription[] { new MqttSubscription(topicFilter, qos) }, null, null,
12011206
new MqttProperties());
12021207
}
1203-
1208+
12041209
/*
12051210
* (non-Javadoc)
12061211
*
@@ -1210,8 +1215,7 @@ public IMqttToken subscribe(String topicFilter, int qos) throws MqttException {
12101215
*/
12111216
@Override
12121217
public IMqttToken subscribe(MqttSubscription subscription) throws MqttException {
1213-
return this.subscribe(new MqttSubscription[] { subscription }, null, null,
1214-
new MqttProperties());
1218+
return this.subscribe(new MqttSubscription[] { subscription }, null, null, new MqttProperties());
12151219
}
12161220

12171221
/*
@@ -1240,9 +1244,13 @@ public IMqttToken subscribe(MqttSubscription[] subscriptions, Object userContext
12401244
MqttProperties subscriptionProperties) throws MqttException {
12411245
final String methodName = "subscribe";
12421246

1243-
// remove any message handlers for individual topics
1247+
// remove any message handlers for individual topics and validate Topics
12441248
for (int i = 0; i < subscriptions.length; ++i) {
12451249
this.comms.removeMessageListener(subscriptions[i].getTopic());
1250+
// Check if the topic filter is valid before subscribing
1251+
MqttTopicValidator.validate(subscriptions[i].getTopic(),
1252+
this.mqttConnection.isWildcardSubscriptionsAvailable(),
1253+
this.mqttConnection.isSharedSubscriptionsAvailable());
12461254
}
12471255

12481256
// Only Generate Log string if we are logging at FINE level
@@ -1253,9 +1261,6 @@ public IMqttToken subscribe(MqttSubscription[] subscriptions, Object userContext
12531261
subs.append(", ");
12541262
}
12551263
subs.append(subscriptions[i].toString());
1256-
1257-
// Check if the topic filter is valid before subscribing
1258-
MqttTopicValidator.validate(subscriptions[i].getTopic(), true/* allow wildcards */);
12591264
}
12601265
// @TRACE 106=Subscribe topicFilter={0} userContext={1} callback={2}
12611266
log.fine(CLASS_NAME, methodName, "106", new Object[] { subs.toString(), userContext, callback });
@@ -1365,14 +1370,15 @@ public IMqttToken subscribe(MqttSubscription[] subscriptions, Object userContext
13651370
int subId = subscriptionProperties.getSubscriptionIdentifiers().get(0);
13661371

13671372
// Automatic Subscription Identifier Assignment is enabled
1368-
if (connOpts.useSubscriptionIdentifiers()) {
1373+
if (connOpts.useSubscriptionIdentifiers() && this.mqttConnection.isSubscriptionIdentifiersAvailable()) {
13691374

13701375
// Application is overriding the subscription Identifier
13711376
if (subId != 0) {
13721377
// Check that we are not already using this ID, else throw Illegal Argument
13731378
// Exception
13741379
if (this.comms.doesSubscriptionIdentifierExist(subId)) {
1375-
throw new IllegalArgumentException(String.format("The Subscription Identifier %s already exists.", subId));
1380+
throw new IllegalArgumentException(
1381+
String.format("The Subscription Identifier %s already exists.", subId));
13761382
}
13771383

13781384
} else {
@@ -1459,7 +1465,7 @@ public IMqttToken unsubscribe(String[] topicFilters, Object userContext, MqttAct
14591465
// Although we already checked when subscribing, but invalid
14601466
// topic filter is meanless for unsubscribing, just prohibit it
14611467
// to reduce unnecessary control packet send to broker.
1462-
MqttTopicValidator.validate(topicFilters[i], true/* allow wildcards */);
1468+
MqttTopicValidator.validate(topicFilters[i], true/* allow wildcards */, this.mqttConnection.isSharedSubscriptionsAvailable());
14631469
}
14641470

14651471
// remove message handlers from the list for this client
@@ -1588,7 +1594,7 @@ public IMqttDeliveryToken publish(String topic, MqttMessage message, Object user
15881594
log.fine(CLASS_NAME, methodName, "111", new Object[] { topic, userContext, callback });
15891595

15901596
// Checks if a topic is valid when publishing a message.
1591-
MqttTopicValidator.validate(topic, false/* wildcards NOT allowed */);
1597+
MqttTopicValidator.validate(topic, false/* wildcards NOT allowed */, true);
15921598

15931599
MqttDeliveryToken token = new MqttDeliveryToken(getClientId());
15941600
token.setActionCallback(callback);

org.eclipse.paho.mqttv5.client/src/main/java/org/eclipse/paho/mqttv5/client/MqttConnectionOptions.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ public void setWill(String topic, MqttMessage message) {
200200
if (topic == null || message == null || message.getPayload() == null) {
201201
throw new IllegalArgumentException();
202202
}
203-
MqttTopicValidator.validate(topic, false); // Wildcards are not allowed
203+
MqttTopicValidator.validate(topic, false, true); // Wildcards are not allowed
204204
this.willDestination = topic;
205205
this.willMessage = message;
206206
// Prevent any more changes to the will message

org.eclipse.paho.mqttv5.common/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttProperties.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1234,7 +1234,7 @@ public String getResponseTopic() {
12341234
*/
12351235
public void setResponseTopic(String responseTopic) {
12361236
if(responseTopic != null) {
1237-
MqttTopicValidator.validate(responseTopic, false);
1237+
MqttTopicValidator.validate(responseTopic, false, true);
12381238
}
12391239
this.responseTopic = responseTopic;
12401240
}

org.eclipse.paho.mqttv5.common/src/main/java/org/eclipse/paho/mqttv5/common/util/MqttTopicValidator.java

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public class MqttTopicValidator {
3838
private static final int MIN_TOPIC_LEN = 1;
3939
private static final int MAX_TOPIC_LEN = 65535;
4040
private static final char NUL = '\u0000';
41-
41+
4242
/**
4343
* Validate the topic name or topic filter
4444
*
@@ -49,7 +49,8 @@ public class MqttTopicValidator {
4949
* @throws IllegalArgumentException
5050
* if the topic is invalid
5151
*/
52-
public static void validate(String topicString, boolean wildcardAllowed) throws IllegalArgumentException {
52+
public static void validate(String topicString, boolean wildcardAllowed, boolean sharedSubAllowed)
53+
throws IllegalArgumentException {
5354
int topicLen = 0;
5455
try {
5556
topicLen = topicString.getBytes("UTF-8").length;
@@ -103,12 +104,18 @@ public static void validate(String topicString, boolean wildcardAllowed) throws
103104
return;
104105
}
105106

107+
// Validate Shared Subscriptions
108+
if (!sharedSubAllowed && topicString.startsWith("$share/")) {
109+
throw new IllegalArgumentException("Shared Subscriptions are not allowed.");
110+
}
111+
106112
// *******************************************************************************
107113
// 2) This is a topic name string that MUST NOT contains any wildcard characters
108114
// *******************************************************************************
109115
if (Strings.containsAny(topicString, TOPIC_WILDCARDS)) {
110116
throw new IllegalArgumentException("The topic name MUST NOT contain any wildcard characters (#+)");
111117
}
118+
112119
}
113120

114121
private static void validateSingleLevelWildcard(String topicString) {
@@ -133,7 +140,7 @@ private static void validateSingleLevelWildcard(String topicString) {
133140
}
134141
}
135142
}
136-
143+
137144
/**
138145
* Check the supplied topic name and filter match
139146
*
@@ -151,8 +158,8 @@ public static boolean isMatched(String topicFilter, String topicName) throws Ill
151158
int topicLen = topicName.length();
152159
int filterLen = topicFilter.length();
153160

154-
MqttTopicValidator.validate(topicFilter, true);
155-
MqttTopicValidator.validate(topicName, false);
161+
MqttTopicValidator.validate(topicFilter, true, true);
162+
MqttTopicValidator.validate(topicName, false, true);
156163

157164
if (topicFilter.equals(topicName)) {
158165
return true;
@@ -179,11 +186,11 @@ public static boolean isMatched(String topicFilter, String topicName) throws Ill
179186
return true;
180187
} else {
181188
/*
182-
* https://github.com/eclipse/paho.mqtt.java/issues/418
183-
* Covers edge case to match sport/# to sport
189+
* https://github.com/eclipse/paho.mqtt.java/issues/418 Covers edge case to
190+
* match sport/# to sport
184191
*/
185-
if ((topicFilter.length() - topicName.length()) == 2 &&
186-
topicFilter.substring(topicFilter.length() -2, topicFilter.length()).equals("/#")) {
192+
if ((topicFilter.length() - topicName.length()) == 2
193+
&& topicFilter.substring(topicFilter.length() - 2, topicFilter.length()).equals("/#")) {
187194
String filterSub = topicFilter.substring(0, topicFilter.length() - 2);
188195
if (filterSub.equals(topicName)) {
189196
System.err.println("filterSub equals topicName: " + filterSub + " == " + topicName);
@@ -193,5 +200,5 @@ public static boolean isMatched(String topicFilter, String topicName) throws Ill
193200
}
194201
return false;
195202
}
196-
203+
197204
}

org.eclipse.paho.mqttv5.common/src/test/java/org/eclipse/paho/mqttv5/common/utils/MqttTopicTest.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public void testValidTopicFilterWildcards() throws Exception {
2929
"/+/sport/+/player1", "#", "/#", "sport/#", "sport/tennis/#" };
3030

3131
for (String topic : topics) {
32-
MqttTopicValidator.validate(topic, true);
32+
MqttTopicValidator.validate(topic, true, true);
3333
}
3434
}
3535

@@ -57,32 +57,32 @@ public void testNonMatchedTopicFilterWildcards() throws Exception {
5757

5858
@Test(expected = IllegalArgumentException.class)
5959
public void testInvalidTopicFilterWildcards1() throws Exception {
60-
MqttTopicValidator.validate("sport/tennis#", true);
60+
MqttTopicValidator.validate("sport/tennis#", true, true);
6161
}
6262

6363
@Test(expected = IllegalArgumentException.class)
6464
public void testInvalidTopicFilterWildcards2() throws Exception {
65-
MqttTopicValidator.validate("sport/tennis/#/ranking", true);
65+
MqttTopicValidator.validate("sport/tennis/#/ranking", true, true);
6666
}
6767

6868
@Test(expected = IllegalArgumentException.class)
6969
public void testInvalidTopicFilterWildcards3() throws Exception {
70-
MqttTopicValidator.validate("sport+", true);
70+
MqttTopicValidator.validate("sport+", true, true);
7171
}
7272

7373
@Test(expected = IllegalArgumentException.class)
7474
public void testInvalidTopicFilterWildcards4() throws Exception {
75-
MqttTopicValidator.validate("sport/+aa", true);
75+
MqttTopicValidator.validate("sport/+aa", true, true);
7676
}
7777

7878
@Test(expected = IllegalArgumentException.class)
7979
public void testInvalidTopicFilterWildcards5() throws Exception {
80-
MqttTopicValidator.validate("sport/#/ball/+/aa", true);
80+
MqttTopicValidator.validate("sport/#/ball/+/aa", true, true);
8181
}
8282

8383
@Test
8484
public void testValidG11NTopic() {
85-
MqttTopicValidator.validate("$shared/葛渚噓/GVTDurTopic02/葛渚噓", true);
85+
MqttTopicValidator.validate("$shared/葛渚噓/GVTDurTopic02/葛渚噓", true, true);
8686
}
8787

8888
}

org.eclipse.paho.mqttv5.common/src/test/java/org/eclipse/paho/mqttv5/common/utils/StringValidationTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public class StringValidationTest {
99

1010
@Test
1111
public void testInvalidTopicFilterWildcards1() throws Exception {
12-
MqttTopicValidator.validate(testString, true);
12+
MqttTopicValidator.validate(testString, true, true);
1313
}
1414

1515
}

0 commit comments

Comments
 (0)