Skip to content

Commit 7aa7ca3

Browse files
uurienhoolioh
andcommitted
String.prototype.replace: String by string (#38)
* String.prototype.replace: String by string * Replace when matcher is a regex * Refactor: Trying to reduce long methods * Refactor replace methods in order to remove maintainability issues. Co-authored-by: Julio Gonzalez <[email protected]>
1 parent 21c987a commit 7aa7ca3

File tree

12 files changed

+893
-9
lines changed

12 files changed

+893
-9
lines changed

binding.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"./src/api/trim.cc",
1717
"./src/api/slice.cc",
1818
"./src/api/substring.cc",
19+
"./src/api/replace.cc",
1920
"./src/iast.cc"
2021
],
2122
"include_dirs" : [

index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@ declare module 'datadog-iast-taint-tracking' {
3030
slice(transactionId: string, result: string, original: string, start: number, end: number): string;
3131
substring(transactionId: string, subject: string, result: string, start: number, end: number): string;
3232
substr(transactionId: string, subject: string, result: string, start: number, length: number): string;
33+
replace(transactionId: string, result: string, thisArg: string, matcher: unknown, replacer: unknown): string;
3334
}
3435
}

index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ try {
2222
},
2323
removeTransaction () {
2424
},
25+
replace (transactionId, result) {
26+
return result
27+
},
2528
concat (transactionId, result) {
2629
return result
2730
},
@@ -49,6 +52,7 @@ const iastNativeMethods = {
4952
getRanges: addon.getRanges,
5053
createTransaction: addon.createTransaction,
5154
removeTransaction: addon.removeTransaction,
55+
replace: require('./replace.js')(addon),
5256
concat: addon.concat,
5357
trim: addon.trim,
5458
trimEnd: addon.trimEnd,

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"index.js",
4949
"index.d.ts",
5050
"prebuilds/**/*",
51+
"replace.js",
5152
"scripts/libc.js",
5253
"LICENSE",
5354
"LICENSE-3rdparty.csv",

replace.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
const isSpecialRegex = /(\$\$)|(\$&)|(\$`)|(\$')|(\$\d)/
2+
3+
function getReplace (addon) {
4+
function isSpecialReplacement (replacer) {
5+
return replacer.indexOf('$') > -1 && !!replacer.match(isSpecialRegex)
6+
}
7+
function shouldBePropagated (transactionId, thisArg, replacer) {
8+
return addon.isTainted(transactionId, thisArg, replacer) && !isSpecialReplacement(replacer)
9+
}
10+
if (addon.replace) {
11+
return addon.replace
12+
}
13+
return function replace (transactionId, result, thisArg, matcher, replacer) {
14+
if (transactionId && typeof thisArg === 'string' && typeof replacer === 'string') {
15+
if (typeof matcher === 'string') {
16+
if (shouldBePropagated(transactionId, thisArg, replacer)) {
17+
const index = thisArg.indexOf(matcher)
18+
if (index > -1) {
19+
return addon.replaceStringByString(transactionId, result, thisArg, matcher, replacer, index)
20+
}
21+
}
22+
} else if (matcher instanceof RegExp) {
23+
if (shouldBePropagated(transactionId, thisArg, replacer)) {
24+
const replacements = []
25+
let lastIndex = -1
26+
for (let match = matcher.exec(thisArg), i = 0; match != null; match = matcher.exec(thisArg), i++) {
27+
const index = match.index
28+
if (index !== lastIndex) {
29+
replacements.push([index, match[0]])
30+
lastIndex = index
31+
} else {
32+
break
33+
}
34+
}
35+
return addon.replaceStringByStringUsingRegex(transactionId, result, thisArg, matcher,
36+
replacer, replacements)
37+
}
38+
}
39+
}
40+
return result
41+
}
42+
}
43+
44+
module.exports = getReplace

src/api/replace.cc

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
/**
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2022 Datadog, Inc.
4+
**/
5+
#include <node.h>
6+
#include <new>
7+
#include <vector>
8+
#include <memory>
9+
10+
#include "replace.h"
11+
#include "../tainted/string_resource.h"
12+
#include "../tainted/range.h"
13+
#include "../tainted/transaction.h"
14+
#include "../iast.h"
15+
#include "../utils/validation_utils.h"
16+
#include "v8.h"
17+
18+
using v8::FunctionCallbackInfo;
19+
using v8::Value;
20+
using v8::Local;
21+
using v8::Context;
22+
using v8::Object;
23+
using v8::Array;
24+
using v8::String;
25+
using v8::Local;
26+
using v8::Integer;
27+
using v8::Int32;
28+
29+
using iast::tainted::Range;
30+
using iast::utils::GetLocalStringPointer;
31+
32+
namespace iast {
33+
namespace api {
34+
35+
struct JsReplacementInfo {
36+
int64_t index;
37+
int64_t matchLength;
38+
int64_t offset;
39+
};
40+
41+
struct MatcherArguments {
42+
Local<Value> result;
43+
Local<Value> self;
44+
Local<Value> matcher;
45+
Local<Value> replacer;
46+
Local<Value> replacements;
47+
};
48+
49+
inline void addReplacerRanges(Transaction* transaction,
50+
SharedRanges* replacerRanges,
51+
int toReplaceStart,
52+
SharedRanges* newRanges) {
53+
if (replacerRanges) {
54+
if (toReplaceStart == 0) {
55+
newRanges->Add(replacerRanges);
56+
} else {
57+
auto replacerItEnd = replacerRanges->end();
58+
for (auto replacerIt = replacerRanges->begin(); replacerItEnd != replacerIt; replacerIt++) {
59+
auto range = (*replacerIt);
60+
newRanges->PushBack(transaction->GetRange(range->start + toReplaceStart,
61+
range->end + toReplaceStart, range->inputInfo));
62+
}
63+
}
64+
}
65+
}
66+
67+
inline SharedRanges* adjustReplacementRanges(Transaction* transaction,
68+
SharedRanges* subjectRanges,
69+
SharedRanges* replacerRanges,
70+
const MatcherArguments& args) {
71+
auto matcherLength = String::Cast(*(args.matcher))->Length();
72+
auto replacementLength = String::Cast(*(args.replacer))->Length();
73+
auto toReplaceStart = Integer::Cast(*(args.replacements))->Value();
74+
int toReplaceEnd = toReplaceStart + matcherLength;
75+
int offset = replacementLength - matcherLength;
76+
auto newRanges = transaction->GetSharedVectorRange();
77+
78+
std::vector<Range*>::iterator subjectIt;
79+
std::vector<Range*>::iterator subjectItEnd;
80+
81+
if (subjectRanges) {
82+
subjectIt = subjectRanges->begin();
83+
subjectItEnd = subjectRanges->end();
84+
while (subjectIt != subjectItEnd && (*subjectIt)->start < toReplaceStart) {
85+
auto range = (*subjectIt);
86+
if (range->end <= toReplaceStart) {
87+
newRanges->PushBack(range);
88+
} else if (range->end > toReplaceStart) {
89+
newRanges->PushBack(transaction->GetRange(range->start, toReplaceStart, range->inputInfo));
90+
break;
91+
}
92+
++subjectIt;
93+
}
94+
}
95+
96+
addReplacerRanges(transaction, replacerRanges, toReplaceStart, newRanges);
97+
98+
if (subjectRanges) {
99+
while (subjectIt != subjectItEnd) {
100+
auto range = (*subjectIt);
101+
if (range->end > toReplaceEnd) {
102+
if (range->start <= toReplaceEnd) {
103+
newRanges->PushBack(transaction->GetRange(toReplaceEnd + offset,
104+
range->end + offset,
105+
range->inputInfo));
106+
} else if (offset == 0) {
107+
newRanges->PushBack(range);
108+
} else {
109+
newRanges->PushBack(transaction->GetRange(range->start + offset,
110+
range->end + offset,
111+
range->inputInfo));
112+
}
113+
}
114+
subjectIt++;
115+
}
116+
}
117+
118+
return newRanges;
119+
}
120+
121+
inline SharedRanges* adjustRegexReplacementRanges(Transaction* transaction,
122+
SharedRanges* subjectRanges,
123+
SharedRanges* replacerRanges,
124+
const MatcherArguments& args,
125+
Local<Context> context) {
126+
std::vector<Range*>::iterator subjectIt;
127+
std::vector<Range*>::iterator subjectItEnd;
128+
auto replacerLen = String::Cast(*(args.replacer))->Length();
129+
auto jsReplacements = Array::Cast(*args.replacements);
130+
auto newRanges = transaction->GetSharedVectorRange();
131+
132+
if (subjectRanges != nullptr) {
133+
subjectIt = subjectRanges->begin();
134+
subjectItEnd = subjectRanges->end();
135+
}
136+
137+
int offset = 0;
138+
int lastEnd = 0;
139+
for (unsigned int i = 0; i < jsReplacements->Length(); i++) {
140+
auto jsReplacement = Object::Cast(*(jsReplacements->Get(context, i).ToLocalChecked()));
141+
auto index = Int32::Cast(*(jsReplacement->Get(context, 0).ToLocalChecked()))->Value();
142+
auto matcherLength = String::Cast(*(jsReplacement->Get(context, 1).ToLocalChecked()))->Length();
143+
struct JsReplacementInfo currentReplacement = {index, matcherLength, replacerLen - matcherLength};
144+
145+
if (subjectRanges) {
146+
while (subjectIt != subjectItEnd && (*subjectIt)->start < index) {
147+
auto breakLoop = false;
148+
auto range = *subjectIt;
149+
if (lastEnd < range->end) {
150+
auto start = range->start;
151+
auto end = range->end;
152+
if (lastEnd > range->start) {
153+
start = lastEnd;
154+
}
155+
start += offset;
156+
if (range->end > index) {
157+
breakLoop = true;
158+
end = index;
159+
}
160+
end += offset;
161+
if (start == range->start && end == range->end) {
162+
newRanges->PushBack(range);
163+
} else {
164+
newRanges->PushBack(transaction->GetRange(start, end, range->inputInfo));
165+
}
166+
}
167+
if (breakLoop) {
168+
break;
169+
}
170+
++subjectIt;
171+
}
172+
}
173+
174+
addReplacerRanges(transaction, replacerRanges, index + offset, newRanges);
175+
176+
lastEnd = currentReplacement.index + currentReplacement.matchLength;
177+
offset = offset + currentReplacement.offset;
178+
}
179+
180+
if (subjectRanges) {
181+
while (subjectIt != subjectItEnd) {
182+
auto range = *subjectIt;
183+
if (lastEnd < range->end) {
184+
if (lastEnd > range->start) {
185+
newRanges->PushBack(transaction->GetRange(lastEnd + offset, range->end + offset,
186+
range->inputInfo));
187+
} else if (offset == 0) {
188+
newRanges->PushBack(range);
189+
} else {
190+
newRanges->PushBack(
191+
transaction->GetRange(range->start + offset, range->end + offset,
192+
range->inputInfo));
193+
}
194+
}
195+
++subjectIt;
196+
}
197+
}
198+
return newRanges;
199+
}
200+
201+
void TaintReplaceStringByStringMethod(const FunctionCallbackInfo<Value>& args) {
202+
if (!utils::ValidateMethodArguments(args, 6, "Wrong number of arguments")) {
203+
return;
204+
}
205+
auto replaceResult = args[1];
206+
if (!replaceResult->IsString()) {
207+
args.GetReturnValue().Set(replaceResult);
208+
return;
209+
}
210+
211+
auto transaction = GetTransaction(GetLocalStringPointer(args[0]));
212+
if (!transaction) {
213+
args.GetReturnValue().Set(replaceResult);
214+
return;
215+
}
216+
217+
try {
218+
MatcherArguments methodArguments = {args[1], args[2], args[3], args[4], args[5]};
219+
220+
auto taintedSubject = transaction->FindTaintedObject(GetLocalStringPointer(methodArguments.self));
221+
auto taintedReplacer = transaction->FindTaintedObject(GetLocalStringPointer(methodArguments.replacer));
222+
auto subjectRanges = (taintedSubject) ? taintedSubject->getRanges() : nullptr;
223+
auto replacerRanges = (taintedReplacer) ? taintedReplacer->getRanges() : nullptr;
224+
225+
if (subjectRanges == nullptr && replacerRanges == nullptr) {
226+
args.GetReturnValue().Set(replaceResult);
227+
return;
228+
}
229+
230+
auto newRanges = adjustReplacementRanges(transaction, subjectRanges, replacerRanges, methodArguments);
231+
if (newRanges->Size() > 0) {
232+
auto isolate = args.GetIsolate();
233+
auto resultString = replaceResult->ToString(isolate->GetCurrentContext()).ToLocalChecked();
234+
auto resultLength = resultString->Length();
235+
if (resultLength == 1) {
236+
replaceResult = tainted::NewExternalString(isolate, replaceResult);
237+
}
238+
auto key = GetLocalStringPointer(replaceResult);
239+
transaction->AddTainted(key, newRanges, replaceResult);
240+
}
241+
} catch (const std::bad_alloc& err) {
242+
} catch (const container::QueuedPoolBadAlloc& err) {
243+
} catch (const container::PoolBadAlloc& err) {
244+
}
245+
args.GetReturnValue().Set(replaceResult);
246+
}
247+
248+
void TaintReplaceStringByStringUsingRegexMethod(const FunctionCallbackInfo<Value>& args) {
249+
// transactionId, result, subject, matcher, replacer, replacements
250+
if (!utils::ValidateMethodArguments(args, 6, "Wrong number of arguments")) {
251+
return;
252+
}
253+
auto replaceResult = args[1];
254+
if (!replaceResult->IsString()) {
255+
args.GetReturnValue().Set(replaceResult);
256+
return;
257+
}
258+
259+
auto transaction = GetTransaction(GetLocalStringPointer(args[0]));
260+
if (!transaction) {
261+
args.GetReturnValue().Set(replaceResult);
262+
return;
263+
}
264+
265+
try {
266+
MatcherArguments methodArguments = {args[1], args[2], args[3], args[4], args[5]};
267+
268+
auto taintedSubject = transaction->FindTaintedObject(GetLocalStringPointer(methodArguments.self));
269+
auto taintedReplacer = transaction->FindTaintedObject(GetLocalStringPointer(methodArguments.replacer));
270+
auto subjectRanges = (taintedSubject) ? taintedSubject->getRanges() : nullptr;
271+
auto replacerRanges = (taintedReplacer) ? taintedReplacer->getRanges() : nullptr;
272+
273+
if (subjectRanges == nullptr && replacerRanges == nullptr) {
274+
args.GetReturnValue().Set(replaceResult);
275+
return;
276+
}
277+
278+
auto newRanges = adjustRegexReplacementRanges(transaction,
279+
subjectRanges,
280+
replacerRanges,
281+
methodArguments,
282+
args.GetIsolate()->GetCurrentContext());
283+
284+
if (newRanges->Size() > 0) {
285+
auto resultString = replaceResult->ToString(args.GetIsolate()->GetCurrentContext()).ToLocalChecked();
286+
auto resultLength = resultString->Length();
287+
if (resultLength == 1) {
288+
replaceResult = tainted::NewExternalString(args.GetIsolate(), replaceResult);
289+
}
290+
auto key = utils::GetLocalStringPointer(replaceResult);
291+
transaction->AddTainted(key, newRanges, replaceResult);
292+
}
293+
} catch (const std::bad_alloc& err) {
294+
} catch (const container::QueuedPoolBadAlloc& err) {
295+
} catch (const container::PoolBadAlloc& err) {
296+
}
297+
args.GetReturnValue().Set(replaceResult);
298+
}
299+
300+
void ReplaceOperations::Init(Local<Object> exports) {
301+
NODE_SET_METHOD(exports, "replaceStringByString", TaintReplaceStringByStringMethod);
302+
NODE_SET_METHOD(exports, "replaceStringByStringUsingRegex", TaintReplaceStringByStringUsingRegexMethod);
303+
}
304+
} // namespace api
305+
} // namespace iast
306+

0 commit comments

Comments
 (0)