Skip to content

Commit 8055c49

Browse files
mohitpubnubMohit Tejanipubnub-release-bot
authored
fix: app_context apis custom field data type to not accept non scalar value (#134)
* app_context: data type update for `custom` fields. To restrict non scaler value for value part of key-value pairs * files: Potential risky channel or files names validation added to prevent path traversal attack via user channel and file name input * format files * fix: unused exception tyeps * PubNub SDK v5.2.1 release. --------- Co-authored-by: Mohit Tejani <mohit.tejani@Mohits-MacBook-Pro.local> Co-authored-by: PubNub Release Bot <120067856+pubnub-release-bot@users.noreply.github.com>
1 parent aa30075 commit 8055c49

15 files changed

Lines changed: 813 additions & 14 deletions

.pubnub.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
---
22
changelog:
3+
- date: 2025-06-25
4+
version: v5.2.1
5+
changes:
6+
- type: bug
7+
text: "Update and validation for `custom` field data type of app context apis to prevent accepting non scalar value."
8+
- type: bug
9+
text: "Added validation for channel and file names to prevent potential various path traversal techniques."
310
- date: 2025-03-12
411
version: v5.2.0
512
changes:
@@ -476,7 +483,7 @@ supported-platforms:
476483
platforms:
477484
- "Dart SDK >=2.6.0 <3.0.0"
478485
version: "PubNub Dart SDK"
479-
version: "5.2.0"
486+
version: "5.2.1"
480487
sdks:
481488
-
482489
full-name: PubNub Dart SDK

pubnub/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## v5.2.1
2+
June 25 2025
3+
4+
#### Fixed
5+
- Update and validation for `custom` field data type of app context apis to prevent accepting non scalar value.
6+
- Added validation for channel and file names to prevent potential various path traversal techniques.
7+
18
## v5.2.0
29
March 12 2025
310

pubnub/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ To add the package to your Dart or Flutter project, add `pubnub` as a dependency
1414

1515
```yaml
1616
dependencies:
17-
pubnub: ^5.2.0
17+
pubnub: ^5.2.1
1818
```
1919
2020
After adding the dependency to `pubspec.yaml`, run the `dart pub get` command in the root directory of your project (the same that the `pubspec.yaml` is in).

pubnub/lib/pubnub.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ export 'src/crypto/crypto.dart' show CryptoModule;
3232
export 'src/crypto/cryptoConfiguration.dart' show CryptoConfiguration;
3333

3434
// DX
35-
export 'src/dx/_utils/utils.dart' show InvariantException;
35+
export 'src/dx/_utils/utils.dart'
36+
show InvariantException, FileValidationException;
3637
export 'src/dx/batch/batch.dart'
3738
show
3839
BatchDx,

pubnub/lib/src/core/core.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class Core {
2121
/// Internal module responsible for supervising.
2222
SupervisorModule supervisor = SupervisorModule();
2323

24-
static String version = '5.2.0';
24+
static String version = '5.2.1';
2525

2626
Core(
2727
{Keyset? defaultKeyset,

pubnub/lib/src/dx/_utils/exceptions.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:pubnub/core.dart';
22
import 'package:pubnub/src/dx/_utils/utils.dart';
33
import 'package:xml/xml.dart';
4+
import 'package:pubnub/src/core/exceptions.dart' as core_exceptions;
45

56
PubNubException getExceptionFromAny(dynamic error) {
67
if (error is DefaultResult) {
@@ -26,7 +27,7 @@ PubNubException getExceptionFromAny(dynamic error) {
2627

2728
PubNubException getExceptionFromDefaultResult(DefaultResult result) {
2829
if (result.status == 400 && result.message == 'Invalid Arguments') {
29-
return InvalidArgumentsException();
30+
return core_exceptions.InvalidArgumentsException();
3031
}
3132

3233
if (result.status == 403 &&
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import 'package:pubnub/core.dart';
2+
3+
/// Custom exception for file validation errors
4+
class FileValidationException extends PubNubException {
5+
FileValidationException(String message) : super(message);
6+
}
7+
8+
/// Validates file-related input parameters to prevent path traversal attacks
9+
/// and other security issues.
10+
class FileValidation {
11+
/// List of dangerous patterns that could lead to path traversal attacks
12+
static const List<String> _dangerousPatterns = [
13+
'../',
14+
'..\\',
15+
'./',
16+
'.\\',
17+
'~/',
18+
'~\\',
19+
];
20+
21+
/// List of dangerous characters that should not be allowed in file names
22+
/// Using actual character codes instead of escape sequences
23+
static final List<int> _dangerousCharacterCodes = [
24+
0, // Null byte
25+
1, // Start of heading
26+
2, // Start of text
27+
3, // End of text
28+
4, // End of transmission
29+
5, // Enquiry
30+
6, // Acknowledge
31+
7, // Bell
32+
8, // Backspace
33+
9, // Tab
34+
10, // Line feed (newline)
35+
11, // Vertical tab
36+
12, // Form feed
37+
13, // Carriage return
38+
14, // Shift out
39+
15, // Shift in
40+
16, // Data link escape
41+
17, // Device control 1
42+
18, // Device control 2
43+
19, // Device control 3
44+
20, // Device control 4
45+
21, // Negative acknowledge
46+
22, // Synchronous idle
47+
23, // End of transmission block
48+
24, // Cancel
49+
25, // End of medium
50+
26, // Substitute
51+
27, // Escape
52+
28, // File separator
53+
29, // Group separator
54+
30, // Record separator
55+
31, // Unit separator
56+
127, // Delete
57+
];
58+
59+
/// Validates a file name for security issues
60+
///
61+
/// Throws [FileValidationException] if the file name contains:
62+
/// - Path traversal patterns (../, ..\, etc.)
63+
/// - Dangerous control characters
64+
/// - Is null, empty, or only whitespace
65+
/// - Contains only dots (., .., etc.)
66+
/// - Exceeds maximum length (255 characters)
67+
static void validateFileName(String? fileName) {
68+
if (fileName == null || fileName.trim().isEmpty) {
69+
throw FileValidationException('File name cannot be null or empty');
70+
}
71+
72+
final trimmedFileName = fileName.trim();
73+
74+
// Check for maximum length (common filesystem limit)
75+
if (trimmedFileName.length > 255) {
76+
throw FileValidationException('File name cannot exceed 255 characters');
77+
}
78+
79+
// Check for dangerous patterns
80+
for (final pattern in _dangerousPatterns) {
81+
if (trimmedFileName.contains(pattern)) {
82+
throw FileValidationException(
83+
'File name contains dangerous path traversal pattern: "$pattern"');
84+
}
85+
}
86+
87+
// Check for dangerous characters
88+
for (final charCode in _dangerousCharacterCodes) {
89+
if (trimmedFileName.contains(String.fromCharCode(charCode))) {
90+
throw FileValidationException(
91+
'File name contains dangerous character: "${charCode.toRadixString(16)}"');
92+
}
93+
}
94+
95+
// Check if filename is only dots (., .., etc.)
96+
if (RegExp(r'^\.*$').hasMatch(trimmedFileName)) {
97+
throw FileValidationException('File name cannot consist only of dots');
98+
}
99+
100+
// Check for reserved names on Windows (even though this is cross-platform)
101+
final reservedNames = [
102+
'CON',
103+
'PRN',
104+
'AUX',
105+
'NUL',
106+
'COM1',
107+
'COM2',
108+
'COM3',
109+
'COM4',
110+
'COM5',
111+
'COM6',
112+
'COM7',
113+
'COM8',
114+
'COM9',
115+
'LPT1',
116+
'LPT2',
117+
'LPT3',
118+
'LPT4',
119+
'LPT5',
120+
'LPT6',
121+
'LPT7',
122+
'LPT8',
123+
'LPT9'
124+
];
125+
126+
final fileNameUpper = trimmedFileName.toUpperCase();
127+
final baseNameUpper = fileNameUpper.split('.').first;
128+
129+
if (reservedNames.contains(baseNameUpper)) {
130+
throw FileValidationException(
131+
'File name cannot be a reserved system name: "$baseNameUpper"');
132+
}
133+
}
134+
135+
/// Validates a file ID for security issues
136+
///
137+
/// Throws [FileValidationException] if the file ID contains:
138+
/// - Path traversal patterns
139+
/// - Dangerous control characters
140+
/// - Is null, empty, or only whitespace
141+
static void validateFileId(String? fileId) {
142+
if (fileId == null || fileId.trim().isEmpty) {
143+
throw FileValidationException('File ID cannot be null or empty');
144+
}
145+
146+
final trimmedFileId = fileId.trim();
147+
148+
// Check for maximum length
149+
if (trimmedFileId.length > 255) {
150+
throw FileValidationException('File ID cannot exceed 255 characters');
151+
}
152+
153+
// Check for dangerous patterns
154+
for (final pattern in _dangerousPatterns) {
155+
if (trimmedFileId.contains(pattern)) {
156+
throw FileValidationException(
157+
'File ID contains dangerous path traversal pattern: "$pattern"');
158+
}
159+
}
160+
161+
// Check for dangerous characters
162+
for (final charCode in _dangerousCharacterCodes) {
163+
if (trimmedFileId.contains(String.fromCharCode(charCode))) {
164+
throw FileValidationException(
165+
'File ID contains dangerous character: "${charCode.toRadixString(16)}"');
166+
}
167+
}
168+
169+
// Check if file ID is only dots
170+
if (RegExp(r'^\.*$').hasMatch(trimmedFileId)) {
171+
throw FileValidationException('File ID cannot consist only of dots');
172+
}
173+
}
174+
175+
/// Validates a channel name for security issues
176+
///
177+
/// Throws [FileValidationException] if the channel name contains:
178+
/// - Path traversal patterns
179+
/// - Dangerous control characters
180+
/// - Is null, empty, or only whitespace
181+
static void validateChannelName(String? channel) {
182+
if (channel == null || channel.trim().isEmpty) {
183+
throw FileValidationException('Channel name cannot be null or empty');
184+
}
185+
186+
final trimmedChannel = channel.trim();
187+
188+
// Check for maximum length
189+
if (trimmedChannel.length > 255) {
190+
throw FileValidationException(
191+
'Channel name cannot exceed 255 characters');
192+
}
193+
194+
// Check for dangerous patterns
195+
for (final pattern in _dangerousPatterns) {
196+
if (trimmedChannel.contains(pattern)) {
197+
throw FileValidationException(
198+
'Channel name contains dangerous path traversal pattern: "$pattern"');
199+
}
200+
}
201+
202+
// Check for dangerous characters
203+
for (final charCode in _dangerousCharacterCodes) {
204+
if (trimmedChannel.contains(String.fromCharCode(charCode))) {
205+
throw FileValidationException(
206+
'Channel name contains dangerous character: "${charCode.toRadixString(16)}"');
207+
}
208+
}
209+
}
210+
}
211+
212+
/// Custom InvalidArgumentsException with specific message
213+
class InvalidArgumentsException extends PubNubException {
214+
static final String _defaultMessage =
215+
'''Invalid Arguments. This may be due to:
216+
- an invalid subscribe key,
217+
- missing or invalid timetoken or channelsTimetoken (values must be greater than 0),
218+
- mismatched number of channels and timetokens,
219+
- invalid characters in a channel name,
220+
- other invalid request data.''';
221+
222+
InvalidArgumentsException() : super(_defaultMessage);
223+
}

pubnub/lib/src/dx/_utils/utils.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export './ensure.dart';
44
export './exceptions.dart';
55
export './signature.dart';
66
export './time.dart';
7+
export './file_validation.dart';

pubnub/lib/src/dx/files/files.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ class FileDx {
6464
dynamic fileMessageMeta,
6565
Keyset? keyset,
6666
String? using}) async {
67+
// Validate input parameters to prevent path traversal attacks
68+
FileValidation.validateChannelName(channel);
69+
FileValidation.validateFileName(fileName);
70+
6771
keyset ??= _core.keysets[using];
6872

6973
var requestPayload = await _core.parser.encode({'name': fileName});
@@ -163,6 +167,9 @@ class FileDx {
163167
String? customMessageType,
164168
Keyset? keyset,
165169
String? using}) async {
170+
// Validate input parameters to prevent path traversal attacks
171+
FileValidation.validateChannelName(channel);
172+
166173
keyset ??= _core.keysets[using];
167174
Ensure(keyset.publishKey).isNotNull('publish key');
168175

@@ -200,6 +207,11 @@ class FileDx {
200207
Future<DownloadFileResult> downloadFile(
201208
String channel, String fileId, String fileName,
202209
{CipherKey? cipherKey, Keyset? keyset, String? using}) async {
210+
// Validate input parameters to prevent path traversal attacks
211+
FileValidation.validateChannelName(channel);
212+
FileValidation.validateFileId(fileId);
213+
FileValidation.validateFileName(fileName);
214+
203215
keyset ??= _core.keysets[using];
204216

205217
return defaultFlow<DownloadFileParams, DownloadFileResult>(
@@ -229,6 +241,9 @@ class FileDx {
229241
/// If that fails as well, then it will throw [InvariantException].
230242
Future<ListFilesResult> listFiles(String channel,
231243
{int? limit, String? next, Keyset? keyset, String? using}) async {
244+
// Validate input parameters to prevent path traversal attacks
245+
FileValidation.validateChannelName(channel);
246+
232247
keyset ??= _core.keysets[using];
233248

234249
return defaultFlow<ListFilesParams, ListFilesResult>(
@@ -246,6 +261,11 @@ class FileDx {
246261
Future<DeleteFileResult> deleteFile(
247262
String channel, String fileId, String fileName,
248263
{Keyset? keyset, String? using}) async {
264+
// Validate input parameters to prevent path traversal attacks
265+
FileValidation.validateChannelName(channel);
266+
FileValidation.validateFileId(fileId);
267+
FileValidation.validateFileName(fileName);
268+
249269
keyset ??= _core.keysets[using];
250270
return defaultFlow<DeleteFileParams, DeleteFileResult>(
251271
keyset: keyset,
@@ -265,6 +285,11 @@ class FileDx {
265285
/// If that fails as well, then it will throw [InvariantException].
266286
Uri getFileUrl(String channel, String fileId, String fileName,
267287
{Keyset? keyset, String? using}) {
288+
// Validate input parameters to prevent path traversal attacks
289+
FileValidation.validateChannelName(channel);
290+
FileValidation.validateFileId(fileId);
291+
FileValidation.validateFileName(fileName);
292+
268293
keyset ??= _core.keysets[using];
269294
var pathSegments = [
270295
'v1',

0 commit comments

Comments
 (0)