Environment
- BiFroMQ
4.0.0
- Verified against
main @ ba2c34e0 — still present on the latest main at the time of filing
- Module:
bifromq-util
- File:
bifromq-util/src/main/java/org/apache/bifromq/util/TopicUtil.java:74-161
Summary
isValidTopicFilter validates the share name and then relies on the main scan loop to validate the real topic filter. When the filter part is empty ("$share/g/"), the loop body never executes and the method returns true.
Root cause
if (topicFilter.startsWith(PREFIX_ORDERED_SHARE) || topicFilter.startsWith(PREFIX_UNORDERED_SHARE)) {
// validate share name
for (i = topicFilter.indexOf(DELIMITER_CHAR) + 1; i < topicFilter.length(); i++) { ... }
if (topicLevelLength == 0) {
return false; // [MQTT-4.8.2-1]
}
if (i == topicFilter.length()) {
return false; // [MQTT-4.8.2-2]
}
topicLevelLength = 0;
// skip one separator to real topicFilter start pos
i++; // :112
}
int startIdx = i;
int level = 1;
for (; i < topicFilter.length(); i++) { // :116 body never runs when i == length
...
}
...
return topicLevelLength <= maxLevelLength; // :161 0 <= maxLevelLength -> true
The two guards at :102 and :106 only constrain the share name. Nothing checks that the filter part is non-empty. For "$share/g/", i is advanced to topicFilter.length(), so the loop at :116 is skipped entirely and the method falls through to the final return.
Verification
Trace for "$share/g/" (length 9):
| Step |
Value |
i = indexOf('/') + 1 |
7 |
scan share name, break on '/' at index 8 |
topicLevelLength = 1 → passes :102 |
i (8) == length (9)? |
no → passes :106 |
i++ → |
9 |
for (; i < 9; i++) |
body not executed |
| final return |
topicLevelLength (0) <= maxLevelLength → true |
Probing the compiled classes confirms isValidTopicFilter("$share/g/", ...) == true while "$share/g", "$share//t", "$share/" and "$share/+/t" are all correctly rejected. "$oshare/g/" behaves the same.
The neighbouring cases having been rejected correctly is what makes this a plain validation gap rather than an intentional relaxation.
Impact
"$share/g/" passes every gate in MQTTSessionHandler.checkAndSubscribe (isWellFormed → true, isValidTopicFilter → true, isWildcardTopicFilter → false), so:
SUBACK is returned with success,
- a topic-filter slot and a shared-subscription route are consumed,
MqttSharedSubNumGauge is incremented,
while the effective filter is the empty string. TopicUtil.from("$share/g/") yields filterLevel=[<empty level>], and the only topic that expands to an empty level is "" — which is itself invalid (TopicUtil.isValidTopic("") == false). No publishable topic can ever match, so the subscriber never receives a message.
Suggested fix
Require the filter part to be non-empty immediately after skipping the separator:
topicLevelLength = 0;
// skip one separator to real topicFilter start pos
i++;
if (i >= topicFilter.length()) {
return false; // "$share/g/" has an empty topic filter
}
}
Environment
4.0.0main@ba2c34e0— still present on the latestmainat the time of filingbifromq-utilbifromq-util/src/main/java/org/apache/bifromq/util/TopicUtil.java:74-161Summary
isValidTopicFiltervalidates the share name and then relies on the main scan loop to validate the real topic filter. When the filter part is empty ("$share/g/"), the loop body never executes and the method returnstrue.Root cause
The two guards at
:102and:106only constrain the share name. Nothing checks that the filter part is non-empty. For"$share/g/",iis advanced totopicFilter.length(), so the loop at:116is skipped entirely and the method falls through to the finalreturn.Verification
Trace for
"$share/g/"(length 9):i = indexOf('/') + 17breakon'/'at index 8topicLevelLength = 1→ passes:102i (8) == length (9)?:106i++→9for (; i < 9; i++)topicLevelLength (0) <= maxLevelLength→trueProbing the compiled classes confirms
isValidTopicFilter("$share/g/", ...) == truewhile"$share/g","$share//t","$share/"and"$share/+/t"are all correctly rejected."$oshare/g/"behaves the same.The neighbouring cases having been rejected correctly is what makes this a plain validation gap rather than an intentional relaxation.
Impact
"$share/g/"passes every gate inMQTTSessionHandler.checkAndSubscribe(isWellFormed→ true,isValidTopicFilter→ true,isWildcardTopicFilter→ false), so:SUBACKis returned with success,MqttSharedSubNumGaugeis incremented,while the effective filter is the empty string.
TopicUtil.from("$share/g/")yieldsfilterLevel=[<empty level>], and the only topic that expands to an empty level is""— which is itself invalid (TopicUtil.isValidTopic("") == false). No publishable topic can ever match, so the subscriber never receives a message.Suggested fix
Require the filter part to be non-empty immediately after skipping the separator: