Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .pubnub.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
---
changelog:
- date: 2025-06-25
version: v5.2.1
changes:
- type: bug
text: "Update and validation for `custom` field data type of app context apis to prevent accepting non scalar value."
- type: bug
text: "Added validation for channel and file names to prevent potential various path traversal techniques."
- date: 2025-03-12
version: v5.2.0
changes:
Expand Down Expand Up @@ -476,7 +483,7 @@ supported-platforms:
platforms:
- "Dart SDK >=2.6.0 <3.0.0"
version: "PubNub Dart SDK"
version: "5.2.0"
version: "5.2.1"
sdks:
-
full-name: PubNub Dart SDK
Expand Down
7 changes: 7 additions & 0 deletions pubnub/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## v5.2.1
June 25 2025

#### Fixed
- Update and validation for `custom` field data type of app context apis to prevent accepting non scalar value.
- Added validation for channel and file names to prevent potential various path traversal techniques.

## v5.2.0
March 12 2025

Expand Down
2 changes: 1 addition & 1 deletion pubnub/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ To add the package to your Dart or Flutter project, add `pubnub` as a dependency

```yaml
dependencies:
pubnub: ^5.2.0
pubnub: ^5.2.1
```

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).
Expand Down
3 changes: 2 additions & 1 deletion pubnub/lib/pubnub.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export 'src/crypto/crypto.dart' show CryptoModule;
export 'src/crypto/cryptoConfiguration.dart' show CryptoConfiguration;

// DX
export 'src/dx/_utils/utils.dart' show InvariantException;
export 'src/dx/_utils/utils.dart'
show InvariantException, FileValidationException;
export 'src/dx/batch/batch.dart'
show
BatchDx,
Expand Down
2 changes: 1 addition & 1 deletion pubnub/lib/src/core/core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Core {
/// Internal module responsible for supervising.
SupervisorModule supervisor = SupervisorModule();

static String version = '5.2.0';
static String version = '5.2.1';

Core(
{Keyset? defaultKeyset,
Expand Down
3 changes: 2 additions & 1 deletion pubnub/lib/src/dx/_utils/exceptions.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:pubnub/core.dart';
import 'package:pubnub/src/dx/_utils/utils.dart';
import 'package:xml/xml.dart';
import 'package:pubnub/src/core/exceptions.dart' as core_exceptions;

PubNubException getExceptionFromAny(dynamic error) {
if (error is DefaultResult) {
Expand All @@ -26,7 +27,7 @@ PubNubException getExceptionFromAny(dynamic error) {

PubNubException getExceptionFromDefaultResult(DefaultResult result) {
if (result.status == 400 && result.message == 'Invalid Arguments') {
return InvalidArgumentsException();
return core_exceptions.InvalidArgumentsException();
}

if (result.status == 403 &&
Expand Down
223 changes: 223 additions & 0 deletions pubnub/lib/src/dx/_utils/file_validation.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import 'package:pubnub/core.dart';

/// Custom exception for file validation errors
class FileValidationException extends PubNubException {
FileValidationException(String message) : super(message);
}

/// Validates file-related input parameters to prevent path traversal attacks
/// and other security issues.
class FileValidation {
/// List of dangerous patterns that could lead to path traversal attacks
static const List<String> _dangerousPatterns = [
'../',
'..\\',
'./',
'.\\',
'~/',
'~\\',
];

/// List of dangerous characters that should not be allowed in file names
/// Using actual character codes instead of escape sequences
static final List<int> _dangerousCharacterCodes = [
0, // Null byte
1, // Start of heading
2, // Start of text
3, // End of text
4, // End of transmission
5, // Enquiry
6, // Acknowledge
7, // Bell
8, // Backspace
9, // Tab
10, // Line feed (newline)
11, // Vertical tab
12, // Form feed
13, // Carriage return
14, // Shift out
15, // Shift in
16, // Data link escape
17, // Device control 1
18, // Device control 2
19, // Device control 3
20, // Device control 4
21, // Negative acknowledge
22, // Synchronous idle
23, // End of transmission block
24, // Cancel
25, // End of medium
26, // Substitute
27, // Escape
28, // File separator
29, // Group separator
30, // Record separator
31, // Unit separator
127, // Delete
];

/// Validates a file name for security issues
///
/// Throws [FileValidationException] if the file name contains:
/// - Path traversal patterns (../, ..\, etc.)
/// - Dangerous control characters
/// - Is null, empty, or only whitespace
/// - Contains only dots (., .., etc.)
/// - Exceeds maximum length (255 characters)
static void validateFileName(String? fileName) {
if (fileName == null || fileName.trim().isEmpty) {
throw FileValidationException('File name cannot be null or empty');
}

final trimmedFileName = fileName.trim();

// Check for maximum length (common filesystem limit)
if (trimmedFileName.length > 255) {
throw FileValidationException('File name cannot exceed 255 characters');
}

// Check for dangerous patterns
for (final pattern in _dangerousPatterns) {
if (trimmedFileName.contains(pattern)) {
throw FileValidationException(
'File name contains dangerous path traversal pattern: "$pattern"');
}
}

// Check for dangerous characters
for (final charCode in _dangerousCharacterCodes) {
if (trimmedFileName.contains(String.fromCharCode(charCode))) {
throw FileValidationException(
'File name contains dangerous character: "${charCode.toRadixString(16)}"');
}
}

// Check if filename is only dots (., .., etc.)
if (RegExp(r'^\.*$').hasMatch(trimmedFileName)) {
throw FileValidationException('File name cannot consist only of dots');
}

// Check for reserved names on Windows (even though this is cross-platform)
final reservedNames = [
'CON',
'PRN',
'AUX',
'NUL',
'COM1',
'COM2',
'COM3',
'COM4',
'COM5',
'COM6',
'COM7',
'COM8',
'COM9',
'LPT1',
'LPT2',
'LPT3',
'LPT4',
'LPT5',
'LPT6',
'LPT7',
'LPT8',
'LPT9'
];

final fileNameUpper = trimmedFileName.toUpperCase();
final baseNameUpper = fileNameUpper.split('.').first;

if (reservedNames.contains(baseNameUpper)) {
throw FileValidationException(
'File name cannot be a reserved system name: "$baseNameUpper"');
}
}

/// Validates a file ID for security issues
///
/// Throws [FileValidationException] if the file ID contains:
/// - Path traversal patterns
/// - Dangerous control characters
/// - Is null, empty, or only whitespace
static void validateFileId(String? fileId) {
if (fileId == null || fileId.trim().isEmpty) {
throw FileValidationException('File ID cannot be null or empty');
}

final trimmedFileId = fileId.trim();

// Check for maximum length
if (trimmedFileId.length > 255) {
throw FileValidationException('File ID cannot exceed 255 characters');
}

// Check for dangerous patterns
for (final pattern in _dangerousPatterns) {
if (trimmedFileId.contains(pattern)) {
throw FileValidationException(
'File ID contains dangerous path traversal pattern: "$pattern"');
}
}

// Check for dangerous characters
for (final charCode in _dangerousCharacterCodes) {
if (trimmedFileId.contains(String.fromCharCode(charCode))) {
throw FileValidationException(
'File ID contains dangerous character: "${charCode.toRadixString(16)}"');
}
}

// Check if file ID is only dots
if (RegExp(r'^\.*$').hasMatch(trimmedFileId)) {
throw FileValidationException('File ID cannot consist only of dots');
}
}

/// Validates a channel name for security issues
///
/// Throws [FileValidationException] if the channel name contains:
/// - Path traversal patterns
/// - Dangerous control characters
/// - Is null, empty, or only whitespace
static void validateChannelName(String? channel) {
if (channel == null || channel.trim().isEmpty) {
throw FileValidationException('Channel name cannot be null or empty');
}

final trimmedChannel = channel.trim();

// Check for maximum length
if (trimmedChannel.length > 255) {
throw FileValidationException(
'Channel name cannot exceed 255 characters');
}

// Check for dangerous patterns
for (final pattern in _dangerousPatterns) {
if (trimmedChannel.contains(pattern)) {
throw FileValidationException(
'Channel name contains dangerous path traversal pattern: "$pattern"');
}
}

// Check for dangerous characters
for (final charCode in _dangerousCharacterCodes) {
if (trimmedChannel.contains(String.fromCharCode(charCode))) {
throw FileValidationException(
'Channel name contains dangerous character: "${charCode.toRadixString(16)}"');
}
}
}
}

/// Custom InvalidArgumentsException with specific message
class InvalidArgumentsException extends PubNubException {
static final String _defaultMessage =
'''Invalid Arguments. This may be due to:
- an invalid subscribe key,
- missing or invalid timetoken or channelsTimetoken (values must be greater than 0),
- mismatched number of channels and timetokens,
- invalid characters in a channel name,
- other invalid request data.''';

InvalidArgumentsException() : super(_defaultMessage);
}
1 change: 1 addition & 0 deletions pubnub/lib/src/dx/_utils/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export './ensure.dart';
export './exceptions.dart';
export './signature.dart';
export './time.dart';
export './file_validation.dart';
25 changes: 25 additions & 0 deletions pubnub/lib/src/dx/files/files.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ class FileDx {
dynamic fileMessageMeta,
Keyset? keyset,
String? using}) async {
// Validate input parameters to prevent path traversal attacks
FileValidation.validateChannelName(channel);
FileValidation.validateFileName(fileName);

keyset ??= _core.keysets[using];

var requestPayload = await _core.parser.encode({'name': fileName});
Expand Down Expand Up @@ -163,6 +167,9 @@ class FileDx {
String? customMessageType,
Keyset? keyset,
String? using}) async {
// Validate input parameters to prevent path traversal attacks
FileValidation.validateChannelName(channel);

keyset ??= _core.keysets[using];
Ensure(keyset.publishKey).isNotNull('publish key');

Expand Down Expand Up @@ -200,6 +207,11 @@ class FileDx {
Future<DownloadFileResult> downloadFile(
String channel, String fileId, String fileName,
{CipherKey? cipherKey, Keyset? keyset, String? using}) async {
// Validate input parameters to prevent path traversal attacks
FileValidation.validateChannelName(channel);
FileValidation.validateFileId(fileId);
FileValidation.validateFileName(fileName);

keyset ??= _core.keysets[using];

return defaultFlow<DownloadFileParams, DownloadFileResult>(
Expand Down Expand Up @@ -229,6 +241,9 @@ class FileDx {
/// If that fails as well, then it will throw [InvariantException].
Future<ListFilesResult> listFiles(String channel,
{int? limit, String? next, Keyset? keyset, String? using}) async {
// Validate input parameters to prevent path traversal attacks
FileValidation.validateChannelName(channel);

keyset ??= _core.keysets[using];

return defaultFlow<ListFilesParams, ListFilesResult>(
Expand All @@ -246,6 +261,11 @@ class FileDx {
Future<DeleteFileResult> deleteFile(
String channel, String fileId, String fileName,
{Keyset? keyset, String? using}) async {
// Validate input parameters to prevent path traversal attacks
FileValidation.validateChannelName(channel);
FileValidation.validateFileId(fileId);
FileValidation.validateFileName(fileName);

keyset ??= _core.keysets[using];
return defaultFlow<DeleteFileParams, DeleteFileResult>(
keyset: keyset,
Expand All @@ -265,6 +285,11 @@ class FileDx {
/// If that fails as well, then it will throw [InvariantException].
Uri getFileUrl(String channel, String fileId, String fileName,
{Keyset? keyset, String? using}) {
// Validate input parameters to prevent path traversal attacks
FileValidation.validateChannelName(channel);
FileValidation.validateFileId(fileId);
FileValidation.validateFileName(fileName);

keyset ??= _core.keysets[using];
var pathSegments = [
'v1',
Expand Down
Loading
Loading