From 288909ffdcee299a24531913a951ac82119c251f Mon Sep 17 00:00:00 2001 From: Carles Capell <107924659+CarlesDD@users.noreply.github.com> Date: Tue, 26 Mar 2024 08:17:04 +0100 Subject: [PATCH] Add propagation string case functions and Array.join function (#98) * String case operations * Array join operator * Fix lint * Fix cpp-lint * Fix cpp-lint * Additional tests for Array.join * Fix test denomination * Fix arguments count * Extract variables * Proper initialization of newRanges for array join * Remove unnecessary conditional * Additional test for String case * Fix lint * Fix to avoid SegFault when checking new ranges * Fix lint --- binding.gyp | 2 + index.d.ts | 2 + index.js | 10 +- src/api/array_join.cc | 145 ++++++++++++++++++ src/api/array_join.h | 21 +++ src/api/string_case.cc | 90 +++++++++++ src/api/string_case.h | 21 +++ src/iast.cc | 4 + test/js/array_join.spec.js | 288 ++++++++++++++++++++++++++++++++++++ test/js/string_case.spec.js | 189 +++++++++++++++++++++++ 10 files changed, 771 insertions(+), 1 deletion(-) create mode 100644 src/api/array_join.cc create mode 100644 src/api/array_join.h create mode 100644 src/api/string_case.cc create mode 100644 src/api/string_case.h create mode 100644 test/js/array_join.spec.js create mode 100644 test/js/string_case.spec.js diff --git a/binding.gyp b/binding.gyp index f39d1e2..b1e6b71 100644 --- a/binding.gyp +++ b/binding.gyp @@ -18,6 +18,8 @@ "./src/api/substring.cc", "./src/api/replace.cc", "./src/api/metrics.cc", + "./src/api/string_case.cc", + "./src/api/array_join.cc", "./src/iast.cc" ], "include_dirs" : [ diff --git a/index.d.ts b/index.d.ts index cb4661c..fcb64b8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -41,5 +41,7 @@ declare module 'datadog-iast-taint-tracking' { substring(transactionId: string, subject: string, result: string, start: number, end: number): string; substr(transactionId: string, subject: string, result: string, start: number, length: number): string; replace(transactionId: string, result: string, thisArg: string, matcher: unknown, replacer: unknown): string; + stringCase(transactionId: string, result: string, thisArg: string): string; + arrayJoin(transactionId: string, result: string, thisArg: any[], separator?: any): string; } } diff --git a/index.js b/index.js index 069e94b..97ae6b2 100644 --- a/index.js +++ b/index.js @@ -55,6 +55,12 @@ try { }, substr (transaction, result) { return result + }, + stringCase (transaction, result) { + return result + }, + arrayJoin (transaction, result) { + return result } } } @@ -75,7 +81,9 @@ const iastNativeMethods = { trimEnd: addon.trimEnd, slice: addon.slice, substring: addon.substring, - substr: addon.substr + substr: addon.substr, + stringCase: addon.stringCase, + arrayJoin: addon.arrayJoin } module.exports = iastNativeMethods diff --git a/src/api/array_join.cc b/src/api/array_join.cc new file mode 100644 index 0000000..ec72c37 --- /dev/null +++ b/src/api/array_join.cc @@ -0,0 +1,145 @@ +/** +* Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. +* This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2022 Datadog, Inc. +**/ +#include +#include +#include +#include + +#include "array_join.h" +#include "../tainted/range.h" +#include "../tainted/string_resource.h" +#include "../tainted/transaction.h" +#include "../iast.h" + +using v8::FunctionCallbackInfo; +using v8::Value; +using v8::Local; +using v8::Isolate; +using v8::Object; +using v8::String; + +using iast::tainted::Range; + +namespace iast { +namespace api { + +const int DEFAULT_JOIN_SEPARATOR_LENGTH = 1; + +void copyRangesWithOffset(Transaction* transaction, + SharedRanges* origRanges, + SharedRanges** destRanges, + int offset) { + if (origRanges != nullptr) { + auto end = origRanges->end(); + for (auto it = origRanges->begin(); it != end; it++) { + auto origRange = *it; + auto newRange = transaction->GetRange( + origRange->start + offset, + origRange->end + offset, + origRange->inputInfo, + origRange->secureMarks); + + if (newRange != nullptr) { + if (*destRanges == nullptr) { + *destRanges = transaction->GetSharedVectorRange(); + } + (*destRanges)->PushBack(newRange); + } else { + break; + } + } + } +} + +SharedRanges* getJoinResultRanges(Isolate* isolate, + Transaction* transaction, v8::Array* arr, + SharedRanges* separatorRanges, + int separatorLength) { + auto length = arr->Length(); + int offset = 0; + SharedRanges* newRanges = nullptr; + auto context = isolate->GetCurrentContext(); + for (uint32_t i = 0; i < length; i++) { + if (i > 0) { + copyRangesWithOffset(transaction, separatorRanges, &newRanges, offset); + offset += separatorLength; + } + auto maybeItem = arr->Get(context, i); + if (!maybeItem.IsEmpty()) { + auto item = maybeItem.ToLocalChecked(); + auto taintedItem = transaction->FindTaintedObject(utils::GetLocalPointer(item)); + auto itemRanges = taintedItem ? taintedItem->getRanges() : nullptr; + copyRangesWithOffset(transaction, itemRanges, &newRanges, offset); + offset += utils::GetLength(isolate, item); + } + } + + return newRanges; +} + +void ArrayJoinOperator(const FunctionCallbackInfo& args) { + auto isolate = args.GetIsolate(); + + if (args.Length() < 3) { + isolate->ThrowException(v8::Exception::TypeError( + v8::String::NewFromUtf8(isolate, + "Wrong number of arguments", + v8::NewStringType::kNormal).ToLocalChecked())); + return; + } + + auto result = args[1]; + + if (!result->IsString()) { + args.GetReturnValue().Set(result); + return; + } + + auto transaction = GetTransaction(utils::GetLocalPointer(args[0])); + if (transaction == nullptr) { + args.GetReturnValue().Set(result); + return; + } + + auto thisArg = args[2]; + if (thisArg->IsObject()) { + auto arrObj = v8::Object::Cast(*thisArg); + if (arrObj->IsArray()) { + try { + int separatorLength = DEFAULT_JOIN_SEPARATOR_LENGTH; + SharedRanges* separatorRanges = nullptr; + if (args.Length() > 3) { + auto separatorArg = args[3]; + auto separatorValue = (*separatorArg); + if (!separatorValue->IsUndefined()) { + auto taintedSeparator = transaction->FindTaintedObject(utils::GetLocalPointer(separatorArg)); + separatorRanges = taintedSeparator ? taintedSeparator->getRanges() : nullptr; + separatorLength = utils::GetCoercedLength(isolate, separatorArg); + } + } + auto arr = v8::Array::Cast(arrObj); + + auto newRanges = getJoinResultRanges(isolate, transaction, arr, separatorRanges, separatorLength); + if (newRanges != nullptr) { + auto key = utils::GetLocalPointer(result); + transaction->AddTainted(key, newRanges, result); + args.GetReturnValue().Set(result); + return; + } + } catch (const std::bad_alloc& err) { + } catch (const container::QueuedPoolBadAlloc& err) { + } catch (const container::PoolBadAlloc& err) { + } + } + } + args.GetReturnValue().Set(args[1]); +} + +void ArrayJoinOperations::Init(Local exports) { + NODE_SET_METHOD(exports, "arrayJoin", ArrayJoinOperator); +} +} // namespace api +} // namespace iast + diff --git a/src/api/array_join.h b/src/api/array_join.h new file mode 100644 index 0000000..5d565fb --- /dev/null +++ b/src/api/array_join.h @@ -0,0 +1,21 @@ +/** +* Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. +* This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2022 Datadog, Inc. +**/ +#ifndef SRC_API_ARRAY_JOIN_H_ +#define SRC_API_ARRAY_JOIN_H_ + +#include +namespace iast { +namespace api { +class ArrayJoinOperations { + public: + static void Init(v8::Local exports); + + private: + ArrayJoinOperations(); + ~ArrayJoinOperations(); +}; +} // namespace api +} // namespace iast +#endif // SRC_API_ARRAY_JOIN_H_ diff --git a/src/api/string_case.cc b/src/api/string_case.cc new file mode 100644 index 0000000..eb35f56 --- /dev/null +++ b/src/api/string_case.cc @@ -0,0 +1,90 @@ +/** +* Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. +* This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2022 Datadog, Inc. +**/ +#include +#include +#include +#include + +#include "string_case.h" +#include "../tainted/range.h" +#include "../tainted/string_resource.h" +#include "../tainted/transaction.h" +#include "../iast.h" + +#define TO_V8STRING(arg) (v8::Local::Cast(arg)) + +using v8::FunctionCallbackInfo; +using v8::Value; +using v8::Local; +using v8::Isolate; +using v8::Object; +using v8::String; + +using iast::tainted::Range; + +namespace iast { +namespace api { + +void StringCaseOperator(const FunctionCallbackInfo& args) { + auto isolate = args.GetIsolate(); + + if (args.Length() < 3) { + isolate->ThrowException(v8::Exception::TypeError( + v8::String::NewFromUtf8(isolate, + "Wrong number of arguments", + v8::NewStringType::kNormal).ToLocalChecked())); + return; + } + if (!args[1]->IsString()) { + args.GetReturnValue().Set(args[1]); + return; + } + + auto transaction = GetTransaction(utils::GetLocalPointer(args[0])); + if (transaction == nullptr) { + args.GetReturnValue().Set(args[1]); + return; + } + + if (args[1] == args[2]) { + args.GetReturnValue().Set(args[1]); + return; + } + + auto taintedObj = transaction->FindTaintedObject(utils::GetLocalPointer(args[2])); + if (!taintedObj) { + args.GetReturnValue().Set(args[1]); + return; + } + + try { + auto ranges = taintedObj->getRanges(); + if (ranges == nullptr) { + args.GetReturnValue().Set(args[1]); + return; + } + + auto res = args[1]; + int resultLength = TO_V8STRING(res)->Length(); + if (resultLength == 1) { + res = tainted::NewExternalString(isolate, res); + } + auto key = utils::GetLocalPointer(res); + transaction->AddTainted(key, ranges, res); + args.GetReturnValue().Set(res); + return; + } catch (const std::bad_alloc& err) { + } catch (const container::QueuedPoolBadAlloc& err) { + } catch (const container::PoolBadAlloc& err) { + } + args.GetReturnValue().Set(args[1]); +} + +void StringCaseOperations::Init(Local exports) { + NODE_SET_METHOD(exports, "stringCase", StringCaseOperator); +} +} // namespace api +} // namespace iast + diff --git a/src/api/string_case.h b/src/api/string_case.h new file mode 100644 index 0000000..a709c22 --- /dev/null +++ b/src/api/string_case.h @@ -0,0 +1,21 @@ +/** +* Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. +* This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2022 Datadog, Inc. +**/ +#ifndef SRC_API_STRING_CASE_H_ +#define SRC_API_STRING_CASE_H_ + +#include +namespace iast { +namespace api { +class StringCaseOperations { + public: + static void Init(v8::Local exports); + + private: + StringCaseOperations(); + ~StringCaseOperations(); +}; +} // namespace api +} // namespace iast +#endif // SRC_API_STRING_CASE_H_ diff --git a/src/iast.cc b/src/iast.cc index 2f7bf04..bec2699 100644 --- a/src/iast.cc +++ b/src/iast.cc @@ -17,6 +17,8 @@ #include "api/substring.h" #include "api/replace.h" #include "api/metrics.h" +#include "api/string_case.h" +#include "api/array_join.h" using transactionManager = iast::container::Singleton>; @@ -50,6 +52,8 @@ void Init(v8::Local exports) { api::SliceOperations::Init(exports); api::Substring::Init(exports); api::ReplaceOperations::Init(exports); + api::StringCaseOperations::Init(exports); + api::ArrayJoinOperations::Init(exports); api::Metrics::Init(exports); exports->GetIsolate()->AddGCEpilogueCallback(iast::gc::OnScavenge, v8::GCType::kGCTypeScavenge); exports->GetIsolate()->AddGCEpilogueCallback(iast::gc::OnMarkSweepCompact, v8::GCType::kGCTypeMarkSweepCompact); diff --git a/test/js/array_join.spec.js b/test/js/array_join.spec.js new file mode 100644 index 0000000..727fb92 --- /dev/null +++ b/test/js/array_join.spec.js @@ -0,0 +1,288 @@ +/** + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. + * This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2022 Datadog, Inc. + **/ +const { TaintedUtils, taintFormattedString, formatTaintedValue } = require('./util') +const assert = require('assert') + +describe('Array join operator', function () { + const id = TaintedUtils.createTransaction('1') + + const rangesTestCases = [ + { + testArray: [':+-a-+:'], + testSeparator: undefined, + joinResult: ':+-a-+:' + }, + { + testArray: [':+-a-+:'], + testSeparator: '###', + joinResult: ':+-a-+:' + }, + { + testArray: [':+-a-+:'], + testSeparator: 123, + joinResult: ':+-a-+:' + }, + { + testArray: [':+-a-+:'], + testSeparator: ':+-###-+:', + joinResult: ':+-a-+:' + }, + { + testArray: [':+-abc-+:', ':+-def-+:'], + testSeparator: undefined, + joinResult: ':+-abc-+:,:+-def-+:' + }, + { + testArray: [':+-abc-+:', ':+-def-+:'], + testSeparator: '###', + joinResult: ':+-abc-+:###:+-def-+:' + }, + { + testArray: [':+-abc-+:', ':+-def-+:'], + testSeparator: 123, + joinResult: ':+-abc-+:123:+-def-+:' + }, + { + testArray: [':+-abc-+:', 'def'], + testSeparator: undefined, + joinResult: ':+-abc-+:,def' + }, + { + testArray: [':+-abc-+:', 'def'], + testSeparator: '###', + joinResult: ':+-abc-+:###def' + }, + { + testArray: [':+-abc-+:', 'def'], + testSeparator: 123, + joinResult: ':+-abc-+:123def' + }, + { + testArray: ['abc', 'def'], + testSeparator: ':+-###-+:', + joinResult: 'abc:+-###-+:def' + }, + { + testArray: ['佫:+-𝒳-+:佫', ':+-😂😂😂-+:', 'abc', '𝒳:+-佫佫佫-+:𝒳'], + testSeparator: undefined, + joinResult: '佫:+-𝒳-+:佫,:+-😂😂😂-+:,abc,𝒳:+-佫佫佫-+:𝒳' + }, + { + testArray: ['佫:+-𝒳-+:佫', ':+-😂😂😂-+:', 'abc', '𝒳:+-佫佫佫-+:𝒳'], + testSeparator: '###', + joinResult: '佫:+-𝒳-+:佫###:+-😂😂😂-+:###abc###𝒳:+-佫佫佫-+:𝒳' + }, + { + testArray: ['佫:+-𝒳-+:佫', ':+-😂😂😂-+:', 'abc', '𝒳:+-佫佫佫-+:𝒳'], + testSeparator: 123, + joinResult: '佫:+-𝒳-+:佫123:+-😂😂😂-+:123abc123𝒳:+-佫佫佫-+:𝒳' + }, + { + testArray: ['佫:+-𝒳-+:佫', ':+-😂😂😂-+:', 'abc', '𝒳:+-佫佫佫-+:𝒳'], + testSeparator: ':+-###-+:', + joinResult: '佫:+-𝒳-+:佫:+-###-+::+-😂😂😂-+::+-###-+:abc:+-###-+:𝒳:+-佫佫佫-+:𝒳' + }, + { + testArray: ['佫:+-𝒳-+:佫', ':+-😂😂😂-+:', 'abc', '𝒳:+-佫佫佫-+:𝒳'], + testSeparator: '佫𝒳😂', + joinResult: '佫:+-𝒳-+:佫佫𝒳😂:+-😂😂😂-+:佫𝒳😂abc佫𝒳😂𝒳:+-佫佫佫-+:𝒳' + }, + { + testArray: ['佫:+-𝒳-+:佫', ':+-😂😂😂-+:', 'abc', '𝒳:+-佫佫佫-+:𝒳'], + testSeparator: '佫:+-𝒳-+:😂', + joinResult: '佫:+-𝒳-+:佫佫:+-𝒳-+:😂:+-😂😂😂-+:佫:+-𝒳-+:😂abc佫:+-𝒳-+:😂𝒳:+-佫佫佫-+:𝒳' + }, + { + testArray: [':+-abc-+:', 'def', 'gh:+-ij-+:kl'], + testSeparator: ':+-###-+:', + joinResult: ':+-abc-+::+-###-+:def:+-###-+:gh:+-ij-+:kl' + }, + { + testArray: [':+-abc-+:', 'def', 'gh:+-ij-+:kl'], + testSeparator: 'AB:+-CD-+:EF', + joinResult: ':+-abc-+:AB:+-CD-+:EFdefAB:+-CD-+:EFgh:+-ij-+:kl' + }, + { + testArray: [':+-abc-+:', 'def', 'gh:+-ij-+:kl'], + testSeparator: ':+-o-+:', + joinResult: ':+-abc-+::+-o-+:def:+-o-+:gh:+-ij-+:kl' + }, + { + testArray: [{ a: 1 }, ':+-abc-+:', 666], + testSeparator: undefined, + joinResult: '[object Object],:+-abc-+:,666' + }, + { + testArray: [{ a: 1 }, ':+-abc-+:', 666], + testSeparator: ':+-o-+:', + joinResult: '[object Object]:+-o-+::+-abc-+::+-o-+:666' + }, + { + testArray: [{ a: 1 }, ':+-abc-+:', 666], + testSeparator: 'AB:+-CD-+:EF', + joinResult: '[object Object]AB:+-CD-+:EF:+-abc-+:AB:+-CD-+:EF666' + }, + { + testArray: [undefined, ':+-abc-+:', 666], + testSeparator: undefined, + joinResult: ',:+-abc-+:,666' + }, + { + testArray: [undefined, ':+-abc-+:', 666], + testSeparator: ':+-o-+:', + joinResult: ':+-o-+::+-abc-+::+-o-+:666' + }, + { + testArray: [undefined, ':+-abc-+:', 666], + testSeparator: 'AB:+-CD-+:EF', + joinResult: 'AB:+-CD-+:EF:+-abc-+:AB:+-CD-+:EF666' + }, + { + testArray: [null, ':+-abc-+:', 666], + testSeparator: undefined, + joinResult: ',:+-abc-+:,666' + }, + { + testArray: [null, ':+-abc-+:', 666], + testSeparator: ':+-o-+:', + joinResult: ':+-o-+::+-abc-+::+-o-+:666' + }, + { + testArray: [null, ':+-abc-+:', 666], + testSeparator: 'AB:+-CD-+:EF', + joinResult: 'AB:+-CD-+:EF:+-abc-+:AB:+-CD-+:EF666' + }, + { + testArray: [[':+-foo-+:', 'bar'], ':+-abc-+:', 666], + testSeparator: ':+-o-+:', + joinResult: ':+-foo-+:,bar:+-o-+::+-abc-+::+-o-+:666' + }, + { + testArray: [null, undefined, { a: 1 }, [':+-foo-+:', '佫:+-𝒳-+:佫', 'bar'], ':+-abc-+:', 666], + testSeparator: ':+-###-+:', + joinResult: ':+-###-+::+-###-+:[object Object]:+-###-+::+-foo-+:,佫:+-𝒳-+:佫,bar:+-###-+::+-abc-+::+-###-+:666' + } + ] + + function testArrayJoinResult (arrayJoinFn, taintedArrayJoinFn) { + let testString = 'sit amet' + let testSeparator = '-' + const testArray = ['lorem', 'ipsum', 1234] + + testString = TaintedUtils.newTaintedString(id, testString, 'PARAM_NAME', 'PARAM_TYPE') + testSeparator = TaintedUtils.newTaintedString(id, testSeparator, 'PARAM_NAME', 'PARAM_TYPE') + + assert.strictEqual(testString, 'sit amet', 'Unexpected value') + assert.equal(true, TaintedUtils.isTainted(id, testString), 'Unexpected value') + assert.strictEqual(testSeparator, '-', 'Unexpected value') + assert.equal(true, TaintedUtils.isTainted(id, testSeparator), 'Unexpected value') + + testArray.push(testString) + + const res = arrayJoinFn.call(testArray, testSeparator) + const ret = taintedArrayJoinFn(id, res, testArray, testSeparator) + assert.equal(res, ret, 'Unexpected vale') + assert.equal(true, TaintedUtils.isTainted(id, ret), 'Unexpected value') + } + + function testArrayJoinNoTaintedResult (arrayJoinFn, taintedArrayJoinFn) { + const testArray = ['lorem', 'ipsum', 1234] + const testSeparator = '-' + const res = arrayJoinFn.call(testArray, testSeparator) + const ret = taintedArrayJoinFn(id, res, testArray, testSeparator) + assert.equal(res, ret, 'Unexpected vale') + assert.equal(false, TaintedUtils.isTainted(id, ret), 'Unexpected value') + } + + function testArrayJoinCheckRanges ( + arrayJoinFn, + taintedArrayJoinFn, + formattedTestArray, + formattedTestSeparator, + expectedResult + ) { + const testArray = formattedTestArray.map((formattedTestString) => taintFormattedString(id, formattedTestString)) + const testSeparator = taintFormattedString(id, formattedTestSeparator) + const res = arrayJoinFn.call(testArray, testSeparator) + const ret = taintedArrayJoinFn(id, res, testArray, testSeparator) + assert.equal(res, ret, 'Unexpected vale') + assert.equal(TaintedUtils.isTainted(id, ret), true, 'Array join returned value not tainted') + + const formattedResult = formatTaintedValue(id, ret) + assert.equal(formattedResult, expectedResult, 'Unexpected ranges') + } + + afterEach(function () { + TaintedUtils.removeTransaction(id) + }) + + describe('Array.join', function () { + it('Wrong arguments', function () { + assert.throws(function () { + TaintedUtils.arrayJoin(id) + }, Error) + }) + + it('Without separator', function () { + assert.doesNotThrow(function () { + TaintedUtils.arrayJoin(id, 'result', ['test', 'array']) + }, Error) + }) + + it('With separator', function () { + assert.doesNotThrow(function () { + TaintedUtils.arrayJoin(id, 'result', ['test', 'array'], 'separator') + }, Error) + }) + + it('With empty array', function () { + TaintedUtils.newTaintedString(id, 'tainted', 'param', 'request') + assert.doesNotThrow(function () { + TaintedUtils.arrayJoin(id, '', []) + }, Error) + }) + + it('Check result', function () { + testArrayJoinResult(Array.prototype.join, TaintedUtils.arrayJoin) + }) + + it('Check result from not tainted value', function () { + testArrayJoinNoTaintedResult(Array.prototype.join, TaintedUtils.arrayJoin) + }) + + it('Secure marks are inherited', () => { + let op1 = 'HELLO' + op1 = TaintedUtils.newTaintedString(id, op1, 'param1', 'REQUEST') + op1 = TaintedUtils.addSecureMarksToTaintedString(id, op1, 0b0110) + + let op2 = 'WORLD' + op2 = TaintedUtils.newTaintedString(id, op2, 'param2', 'REQUEST') + op2 = TaintedUtils.addSecureMarksToTaintedString(id, op2, 0b0100) + + let separator = ' --- ' + separator = TaintedUtils.newTaintedString(id, separator, 'separator', 'REQUEST') + separator = TaintedUtils.addSecureMarksToTaintedString(id, separator, 0b0101) + + const arrayTest = [op1, op2] + + let result = arrayTest.join(separator) + result = TaintedUtils.arrayJoin(id, result, arrayTest, separator) + + const ranges = TaintedUtils.getRanges(id, result) + assert.equal(ranges.length, 3) + assert.equal(ranges[0].secureMarks, 0b0110) + assert.equal(ranges[1].secureMarks, 0b0101) + assert.equal(ranges[2].secureMarks, 0b0100) + }) + + describe('Check ranges', function () { + rangesTestCases.forEach(({ testArray, testSeparator, joinResult }) => { + it(`Test ${joinResult}`, () => { + testArrayJoinCheckRanges(Array.prototype.join, TaintedUtils.arrayJoin, testArray, testSeparator, joinResult) + }) + }) + }) + }) +}) diff --git a/test/js/string_case.spec.js b/test/js/string_case.spec.js new file mode 100644 index 0000000..dbff28d --- /dev/null +++ b/test/js/string_case.spec.js @@ -0,0 +1,189 @@ +/** + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. + * This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2022 Datadog, Inc. + **/ +const { TaintedUtils, taintFormattedString, formatTaintedValue } = require('./util') +const assert = require('assert') + +describe('String case operator', function () { + const id = TaintedUtils.createTransaction('1') + + const rangesTestCases = [ + { + testString: ':+-abcdef-+:', + toLowerResult: ':+-abcdef-+:', + toUpperResult: ':+-ABCDEF-+:' + }, + { + testString: ':+-ABCDEF-+:', + toLowerResult: ':+-abcdef-+:', + toUpperResult: ':+-ABCDEF-+:' + }, + { + testString: ':+-ABCdef-+:', + toLowerResult: ':+-abcdef-+:', + toUpperResult: ':+-ABCDEF-+:' + }, + { + testString: ':+-ABC-+::+-def-+:', + toLowerResult: ':+-abc-+::+-def-+:', + toUpperResult: ':+-ABC-+::+-DEF-+:' + }, + { + testString: ':+-ABC-+:dEf:+-ghi-+:', + toLowerResult: ':+-abc-+:def:+-ghi-+:', + toUpperResult: ':+-ABC-+:DEF:+-GHI-+:' + }, + { + testString: 'ABC:+-def-+:', + toLowerResult: 'abc:+-def-+:', + toUpperResult: 'ABC:+-DEF-+:' + }, + { + testString: ':+-ABC-+:def', + toLowerResult: ':+-abc-+:def', + toUpperResult: ':+-ABC-+:DEF' + }, + { + testString: ':+-佫😂😂😂𝒳-+:', + toLowerResult: ':+-佫😂😂😂𝒳-+:', + toUpperResult: ':+-佫😂😂😂𝒳-+:' + }, + { + testString: ':+-佫-+::+-😂😂😂-+::+-𝒳-+:', + toLowerResult: ':+-佫-+::+-😂😂😂-+::+-𝒳-+:', + toUpperResult: ':+-佫-+::+-😂😂😂-+::+-𝒳-+:' + }, + { + testString: 'ABC佫:+-#😂l😂M😂#-+:𝒳xyz', + toLowerResult: 'abc佫:+-#😂l😂m😂#-+:𝒳xyz', + toUpperResult: 'ABC佫:+-#😂L😂M😂#-+:𝒳XYZ' + } + ] + + function testStringCaseResult (stringCaseFn, taintedStringCaseFn) { + let testString = 'abcDEF' + + testString = TaintedUtils.newTaintedString(id, testString, 'PARAM_NAME', 'PARAM_TYPE') + assert.strictEqual(testString, 'abcDEF', 'Unexpected value') + assert.equal(true, TaintedUtils.isTainted(id, testString), 'Unexpected value') + + const res = stringCaseFn.call(testString) + const ret = taintedStringCaseFn(id, res, testString) + assert.equal(res, ret, 'Unexpected vale') + assert.equal(true, TaintedUtils.isTainted(id, ret), 'Unexpected value') + } + + function testStringCaseNoTaintedResult (stringCaseFn, taintedStringCaseFn) { + const testString = 'abcDEF' + const res = stringCaseFn.call(testString) + const ret = taintedStringCaseFn(id, res, testString) + assert.equal(res, ret, 'Unexpected vale') + assert.equal(false, TaintedUtils.isTainted(id, ret), 'Unexpected value') + } + + function testStringCaseCheckRanges (stringCaseFn, taintedStringCaseFn, formattedTestString, expectedResult) { + const testString = taintFormattedString(id, formattedTestString) + const res = stringCaseFn.call(testString) + assert.equal(TaintedUtils.isTainted(id, testString), true, 'Test string not tainted') + const ret = taintedStringCaseFn(id, res, testString) + assert.equal(res, ret, 'Unexpected vale') + assert.equal(TaintedUtils.isTainted(id, ret), true, 'String case returned value not tainted') + + const formattedResult = formatTaintedValue(id, ret) + assert.equal(formattedResult, expectedResult, 'Unexpected ranges') + } + + function testStringCaseOneCharResultIsDifferentInstance (stringCaseFn, taintedStringCaseFn, testString) { + const paramValue = TaintedUtils.newTaintedString(id, testString, 'PARAM_NAME', 'PARAM_TYPE') + assert.equal(true, TaintedUtils.isTainted(id, paramValue)) + assert.equal(false, TaintedUtils.isTainted(id, testString)) + const originalStringCase = stringCaseFn.call(testString) + let res = stringCaseFn.call(paramValue) + res = taintedStringCaseFn(id, res, paramValue) + assert.equal(true, TaintedUtils.isTainted(id, res)) + assert.equal(false, TaintedUtils.isTainted(id, originalStringCase)) + } + + afterEach(function () { + TaintedUtils.removeTransaction(id) + }) + + describe('stingCase', function () { + it('Wrong arguments stringCase', function () { + assert.throws(function () { + TaintedUtils.stringCase(id) + }, Error) + }) + }) + + describe('toLowerCase', function () { + it('Check result', function () { + testStringCaseResult(String.prototype.toLowerCase, TaintedUtils.stringCase) + }) + + it('One char length results, different instance', function () { + testStringCaseOneCharResultIsDifferentInstance(String.prototype.toLowerCase, TaintedUtils.stringCase, 'A') + }) + + it('Check result from not tainted value', function () { + testStringCaseNoTaintedResult(String.prototype.toLowerCase, TaintedUtils.stringCase) + }) + + it('Secure marks are inherited', () => { + let op1 = 'HELLO WORLD' + op1 = TaintedUtils.newTaintedString(id, op1, 'param1', 'REQUEST') + op1 = TaintedUtils.addSecureMarksToTaintedString(id, op1, 0b0110) + + let result = op1.toLowerCase() + result = TaintedUtils.stringCase(id, result, op1) + + const ranges = TaintedUtils.getRanges(id, result) + assert.equal(ranges.length, 1) + assert.equal(ranges[0].secureMarks, 0b0110) + }) + + describe('Check ranges', function () { + rangesTestCases.forEach(({ testString, toLowerResult }) => { + it(`Test ${testString}`, () => { + testStringCaseCheckRanges(String.prototype.toLowerCase, TaintedUtils.stringCase, testString, toLowerResult) + }) + }) + }) + }) + + describe('toUpperCase', function () { + it('Check result', function () { + testStringCaseResult(String.prototype.toUpperCase, TaintedUtils.stringCase) + }) + + it('One char length results, different instance', function () { + testStringCaseOneCharResultIsDifferentInstance(String.prototype.toUpperCase, TaintedUtils.stringCase, 'A') + }) + + it('Check result from not tainted value', function () { + testStringCaseNoTaintedResult(String.prototype.toUpperCase, TaintedUtils.stringCase) + }) + + it('Secure marks are inherited', () => { + let op1 = 'HELLO WORLD' + op1 = TaintedUtils.newTaintedString(id, op1, 'param1', 'REQUEST') + op1 = TaintedUtils.addSecureMarksToTaintedString(id, op1, 0b0110) + + let result = op1.toUpperCase() + result = TaintedUtils.stringCase(id, result, op1) + + const ranges = TaintedUtils.getRanges(id, result) + assert.equal(ranges.length, 1) + assert.equal(ranges[0].secureMarks, 0b0110) + }) + + describe('Check ranges', function () { + rangesTestCases.forEach(({ testString, toUpperResult }) => { + it(`Test ${testString}`, () => { + testStringCaseCheckRanges(String.prototype.toUpperCase, TaintedUtils.stringCase, testString, toUpperResult) + }) + }) + }) + }) +})