From 4dce8d31671385b8d1da1bade308dd30f2fa7a3e Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Wed, 19 Mar 2025 16:17:28 +0530 Subject: [PATCH 1/4] fix: code vulnerabilitiies --- lib/src/base_query.dart | 18 ++++++++++++++++-- lib/src/entry_queryable.dart | 27 +++++++++++++-------------- lib/src/query_params.dart | 31 +++++++++++++++++-------------- lib/src/stack.dart | 2 -- 4 files changed, 46 insertions(+), 32 deletions(-) diff --git a/lib/src/base_query.dart b/lib/src/base_query.dart index 29086a6..e752775 100644 --- a/lib/src/base_query.dart +++ b/lib/src/base_query.dart @@ -156,8 +156,9 @@ class BaseQuery { } void where(String fieldUid, QueryOperation queryOperation) { - if (fieldUid.isNotEmpty) { - switch(queryOperation.operationType) { + if (fieldUid.isNotEmpty && _isValidFieldUid(fieldUid) + && _isValidQueryValue(queryOperation.value)) { + switch (queryOperation.operationType) { case QueryOperationType.Equals: parameter[fieldUid] = queryOperation.value; break; @@ -191,4 +192,17 @@ class BaseQuery { } } } + + bool _isValidFieldUid(String fieldUid) { + final validFieldUidPattern = RegExp(r'^[a-zA-Z0-9-_.]+$'); + return validFieldUidPattern.hasMatch(fieldUid); + } + + bool _isValidQueryValue(dynamic value) { + if (value is String) { + final validValuePattern = RegExp(r'^[a-zA-Z0-9-_.]+$'); + return validValuePattern.hasMatch(value); + } + return true; + } } diff --git a/lib/src/entry_queryable.dart b/lib/src/entry_queryable.dart index afee9c3..ce99575 100644 --- a/lib/src/entry_queryable.dart +++ b/lib/src/entry_queryable.dart @@ -1,4 +1,4 @@ -// ignore_for_file: lines_longer_than_80_chars +// ignore_for_file: lines_longer_than_80_chars, unnecessary_lambdas import 'package:contentstack/constant.dart'; import 'package:contentstack/src/enums/include.dart'; @@ -8,6 +8,11 @@ import 'package:contentstack/src/enums/include_type.dart'; class EntryQueryable { Map parameter = {}; + //sanitize + String sanitizeInput(String input) { + return input.replaceAll(RegExp(r'[^a-zA-Z0-9-_.]'), ''); +} + /// /// This method adds key and value to an Entry. /// [key] The key as string which needs to be added to an Entry @@ -43,10 +48,7 @@ class EntryQueryable { /// void except(List fieldUid) { if (fieldUid.isNotEmpty) { - final List referenceArray = []; - for (final item in fieldUid) { - referenceArray.add(item); - } + final List referenceArray = fieldUid.map((item) => sanitizeInput(item)).toList(); parameter['except[BASE][]'] = referenceArray.toString(); } } @@ -171,15 +173,15 @@ class EntryQueryable { case IncludeType.None: if (referenceFieldUid.runtimeType == List) { for (var uid in referenceFieldUid) { - referenceArray.add(uid); + referenceArray.add(sanitizeInput(uid)); } } else if (referenceFieldUid.runtimeType == String) { - referenceArray.add(referenceFieldUid); + referenceArray.add(sanitizeInput(referenceFieldUid)); } if (includeReferenceField.fieldUidList.isNotEmpty) { for (final item in includeReferenceField.fieldUidList) { - referenceArray.add(item); + referenceArray.add(sanitizeInput(item)); } } parameter['include[]'] = referenceArray.toString(); @@ -188,7 +190,7 @@ class EntryQueryable { final Map referenceOnlyParam = {}; if (includeReferenceField.fieldUidList.isNotEmpty) { for (final item in includeReferenceField.fieldUidList) { - referenceArray.add(item); + referenceArray.add(sanitizeInput(item)); } } referenceOnlyParam[referenceFieldUid] = referenceArray; @@ -199,7 +201,7 @@ class EntryQueryable { final Map referenceOnlyParam = {}; if (includeReferenceField.fieldUidList.isNotEmpty) { for (final item in includeReferenceField.fieldUidList) { - referenceArray.add(item); + referenceArray.add(sanitizeInput(item)); } } referenceOnlyParam[referenceFieldUid] = referenceArray; @@ -262,10 +264,7 @@ class EntryQueryable { /// void only(List fieldUid) { if (fieldUid.isNotEmpty) { - final List referenceArray = []; - for (final item in fieldUid) { - referenceArray.add(item); - } + final List referenceArray = fieldUid.map((item) => sanitizeInput(item)).toList(); parameter['only[BASE][]'] = referenceArray.toString(); } } diff --git a/lib/src/query_params.dart b/lib/src/query_params.dart index ca817e5..10898ea 100644 --- a/lib/src/query_params.dart +++ b/lib/src/query_params.dart @@ -5,7 +5,8 @@ class URLQueryParams { // Appends a parameter to the query with received key. void append(String key, dynamic value) { if (value != null && value.toString().isNotEmpty) { - _values[key] = Uri.encodeQueryComponent(value.toString()); + final sanitizedValue = _sanitizeInput(value.toString()); + _values[key] = Uri.encodeQueryComponent(sanitizedValue); } } @@ -18,20 +19,22 @@ class URLQueryParams { // * param1=value1¶m2=value2 @override String toString() { - String response = ''; - _values.forEach((key, value) { - response += '$key=$value&'; - }); - return response.substring(0, response.isEmpty ? 0 : response.length - 1); + return _values.entries + .map((entry) => '${Uri.encodeQueryComponent(entry.key)}=${entry.value}') + .join('&'); } - String toUrl(String urls) { - String updatedUrl; - if (urls.isNotEmpty && urls.endsWith('/')) { - updatedUrl = urls.substring(0, urls.length - 1); - } else { - updatedUrl = urls; - } - return '$updatedUrl?${toString()}'; + String toUrl(String url) { + if (url.isEmpty) throw ArgumentError('URL cannot be empty'); + + final Uri parsedUri = Uri.parse(url); + final String normalizedUrl = parsedUri.normalizePath().toString(); + + return Uri.parse('$normalizedUrl?${toString()}').toString(); + } + + String _sanitizeInput(String input) { + final pattern = RegExp(r'[<>\"\;(){}]'); + return input.replaceAll(pattern, ''); } } diff --git a/lib/src/stack.dart b/lib/src/stack.dart index 5f9d213..6d0c3e1 100644 --- a/lib/src/stack.dart +++ b/lib/src/stack.dart @@ -463,8 +463,6 @@ class Stack { 'authorization': headers!['authorization']!, 'api_key': headers!['api_key']!, }; - - print('Request URL: $_url'); await http.get(Uri.parse(_url), headers: _headers).then((response) { final Map bodyJson = json.decode(utf8.decode(response.bodyBytes)); print(bodyJson); From 951c2b7dbc23400f1c5b88863ec7bc1cd18ee900 Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Fri, 21 Mar 2025 12:53:30 +0530 Subject: [PATCH 2/4] fixes 2 --- lib/src/base_query.dart | 10 +--------- test/query_test.dart | 1 + test/stack_test.dart | 2 +- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/lib/src/base_query.dart b/lib/src/base_query.dart index e752775..0fe9efa 100644 --- a/lib/src/base_query.dart +++ b/lib/src/base_query.dart @@ -156,8 +156,7 @@ class BaseQuery { } void where(String fieldUid, QueryOperation queryOperation) { - if (fieldUid.isNotEmpty && _isValidFieldUid(fieldUid) - && _isValidQueryValue(queryOperation.value)) { + if(_isValidFieldUid(fieldUid)) { switch (queryOperation.operationType) { case QueryOperationType.Equals: parameter[fieldUid] = queryOperation.value; @@ -198,11 +197,4 @@ class BaseQuery { return validFieldUidPattern.hasMatch(fieldUid); } - bool _isValidQueryValue(dynamic value) { - if (value is String) { - final validValuePattern = RegExp(r'^[a-zA-Z0-9-_.]+$'); - return validValuePattern.hasMatch(value); - } - return true; - } } diff --git a/test/query_test.dart b/test/query_test.dart index 79dd189..5a673ed 100644 --- a/test/query_test.dart +++ b/test/query_test.dart @@ -394,6 +394,7 @@ void main() { await query.find().then((response) { final completeUrl = query.getQueryUrl()['query']; //(response.toString()); + print(completeUrl.toString()); expect('{\"\$or\":[{\"title\":\"Room 13\"},{\"number\":20}]}', completeUrl); }).catchError((onError) { diff --git a/test/stack_test.dart b/test/stack_test.dart index 915b686..c96a13b 100644 --- a/test/stack_test.dart +++ b/test/stack_test.dart @@ -143,7 +143,7 @@ void main() { group('testcase for URLQueryParams', () { test('test query_params', () { final params = URLQueryParams()..append('key', 'value'); - final url = params.toUrl('cdn.contentstack.io/'); + final url = params.toUrl('cdn.contentstack.io'); expect('cdn.contentstack.io?key=value', url); }); From 6fefac78d7ee6297bca19e6931757be9c223b78b Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Fri, 21 Mar 2025 13:26:51 +0530 Subject: [PATCH 3/4] changes made --- test/query_test.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/query_test.dart b/test/query_test.dart index 5a673ed..4f07853 100644 --- a/test/query_test.dart +++ b/test/query_test.dart @@ -393,8 +393,6 @@ void main() { query.operator(QueryOperator(QueryOperatorType.Or, listOfQuery)); await query.find().then((response) { final completeUrl = query.getQueryUrl()['query']; - //(response.toString()); - print(completeUrl.toString()); expect('{\"\$or\":[{\"title\":\"Room 13\"},{\"number\":20}]}', completeUrl); }).catchError((onError) { From 4fee83a6a31fad00eb9df8eb75d4062c9c560337 Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Fri, 21 Mar 2025 13:27:58 +0530 Subject: [PATCH 4/4] version bump --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 437c375..841e439 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +### **APR-01-2025** + +#### v1.0.1 - SRE fixes + ### **FEB-17-2025** #### v1.0.0 - v3 migration null support added diff --git a/pubspec.yaml b/pubspec.yaml index 1099cea..17fb2db 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: contentstack description: Contentstack is a headless CMS with an API-first approach that puts content at the centre. -version: 1.0.0 +version: 1.0.1 homepage: https://www.contentstack.com documentation: https://www.contentstack.com/docs/developers/apis/content-delivery-api