Skip to content

Commit 0450147

Browse files
committed
prettify request response body in vulnerable api findings
1 parent 13a2d66 commit 0450147

File tree

7 files changed

+232
-16
lines changed

7 files changed

+232
-16
lines changed

app/components/file-details/vulnerability-analysis-details/findings/code-box/index.hbs

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737
<pre
3838
id={{this.copyTargetId}}
39-
local-class='vulnerability-finding-description'
39+
local-class='vulnerability-finding-description {{this.whiteSpace}}'
4040
data-test-analysisDetails-vulFinding-code
4141
>{{yield}}
4242
</pre>

app/components/file-details/vulnerability-analysis-details/findings/code-box/index.scss

+8-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
text-overflow: clip;
3030
background-color: unset;
3131
color: var(--file-details-vulnerability-analysis-details-findings-code-box-color-primary);
32-
white-space: pre-line;
3332
line-height: normal;
3433
padding: 0.75em;
3534
}
@@ -40,6 +39,14 @@
4039
}
4140
}
4241

42+
.pre-wrap {
43+
white-space: pre-wrap;
44+
}
45+
46+
.pre-line {
47+
white-space: pre-line;
48+
}
49+
4350
.analysis-overridded-passed .vulnerability-finding-description {
4451
background-color: var(
4552
--file-details-vulnerability-analysis-details-findings-code-box-marked-passed-code-background-color

app/components/file-details/vulnerability-analysis-details/findings/code-box/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface FileDetailsVulnerabilityAnalysisDetailsFindingsCodeBoxSignature
1010
title: string;
1111
copyIcon?: boolean;
1212
markedAsPassed?: boolean;
13+
whiteSpace?: 'pre-wrap' | 'pre-line';
1314
};
1415
Blocks: {
1516
default: [];
@@ -24,6 +25,10 @@ export default class FileDetailsVulnerabilityAnalysisDetailsFindingsCodeBoxCompo
2425
return `copy-target-${guidFor(this)}`;
2526
}
2627

28+
get whiteSpace() {
29+
return this.args.whiteSpace || 'pre-line';
30+
}
31+
2732
@action
2833
handleCopySuccess(event: ClipboardJS.Event) {
2934
this.notify.info(this.intl.t('copiedToClipboard'));

app/components/file-details/vulnerability-analysis-details/findings/vulnerable-api/index.hbs

+3-2
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,15 @@
3434
'file-details/vulnerability-analysis-details/findings/code-box'
3535
title=detail.title
3636
copyIcon=detail.copyIcon
37+
whiteSpace=detail.whiteSpace
3738
markedAsPassed=@analysis.isOverriddenAsPassed
3839
)
3940
as |CodeBox|
4041
}}
4142
{{#if detail.isKeyValuePair}}
4243
{{! Note: formating will add a new line for each key value pair }}
43-
<CodeBox>{{#each-in detail.value as |key value|}}<span>{{key}}:
44-
{{value}}</span>
44+
<CodeBox>{{#each-in detail.value as |key value|}}<span
45+
>{{key}}:&nbsp;{{value}}</span>
4546
{{/each-in}}
4647
</CodeBox>
4748
{{else}}

app/components/file-details/vulnerability-analysis-details/findings/vulnerable-api/index.ts

+52-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Component from '@glimmer/component';
22
import { service } from '@ember/service';
3+
import { action } from '@ember/object';
34
import type IntlService from 'ember-intl/services/intl';
45

56
import type AnalysisModel from 'irene/models/analysis';
@@ -13,6 +14,20 @@ export interface FileDetailsVulnerabilityAnalysisDetailsFindingsVulnerableApiSig
1314
};
1415
}
1516

17+
interface FormattedResult {
18+
value: string;
19+
isJSON?: boolean;
20+
}
21+
22+
interface VulnerabilityDetails {
23+
title: string;
24+
value: string;
25+
isEmpty: boolean;
26+
copyIcon: boolean;
27+
isKeyValuePair?: boolean;
28+
whiteSpace?: 'pre-wrap' | 'pre-line';
29+
}
30+
1631
export default class FileDetailsVulnerabilityAnalysisDetailsFindingsVulnerableApiComponent extends Component<FileDetailsVulnerabilityAnalysisDetailsFindingsVulnerableApiSignature> {
1732
@service declare intl: IntlService;
1833

@@ -113,16 +128,49 @@ export default class FileDetailsVulnerabilityAnalysisDetailsFindingsVulnerableAp
113128
}
114129
}
115130

131+
@action getFormattedText(inputString: string | undefined): FormattedResult {
132+
if (!inputString) {
133+
return {
134+
value: '',
135+
};
136+
}
137+
138+
const sanitizedString = inputString
139+
.trim()
140+
.replace(/(^['"])|(['"]$)/g, '')
141+
.replace(/\\n/g, '\n');
142+
143+
// Try to parse as JSON first
144+
try {
145+
const parsed = JSON.parse(sanitizedString);
146+
147+
return {
148+
value: JSON.stringify(parsed, null, 2),
149+
isJSON: true,
150+
};
151+
} catch {
152+
// If JSON parsing fails, return the sanitized string
153+
return {
154+
value: sanitizedString,
155+
isJSON: false,
156+
};
157+
}
158+
}
159+
116160
get vulnerabilityDetails() {
117161
const request = this.args.currentVulnerability?.request;
118162
const response = this.args.currentVulnerability?.response;
119163

164+
const formattedRequestBody = this.getFormattedText(request?.body);
165+
const formattedResponseBody = this.getFormattedText(response?.text);
166+
120167
return [
121168
{
122169
title: this.intl.t('requestBody'),
123-
value: request?.body,
170+
value: formattedRequestBody.value,
124171
isEmpty: this.isRequestBodyEmpty,
125172
copyIcon: true,
173+
whiteSpace: formattedRequestBody.isJSON ? 'pre-wrap' : 'pre-line',
126174
},
127175
{
128176
title: this.intl.t('requestHeaders'),
@@ -167,11 +215,12 @@ export default class FileDetailsVulnerabilityAnalysisDetailsFindingsVulnerableAp
167215
},
168216
{
169217
title: this.intl.t('responseBody'),
170-
value: response?.text,
218+
value: formattedResponseBody.value,
171219
isEmpty: this.isResponseBodyEmpty,
220+
whiteSpace: formattedResponseBody.isJSON ? 'pre-wrap' : 'pre-line',
172221
copyIcon: true,
173222
},
174-
];
223+
] as VulnerabilityDetails[];
175224
}
176225
}
177226

app/utils/parse-vulnerable-api-finding.ts

+69-9
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,39 @@ function splitVulnerableApiFindingIntoBlocks(content: string): string[] {
132132
return content.split(/\n{2,3}/);
133133
}
134134

135+
function handleBodyField(
136+
key: string,
137+
value: string,
138+
lines: string[],
139+
i: number
140+
): { updatedBuffer: string; updatedIndex: number } {
141+
let currentBuffer = value;
142+
let nextIndex = i + 1;
143+
144+
// Check if body value is a multi-line JSON or array
145+
if (
146+
(value.startsWith("'{") && !value.endsWith("}'")) ||
147+
(value.startsWith("'[") && !value.endsWith("]'"))
148+
) {
149+
// Keep reading lines until the closing quote
150+
while (
151+
nextIndex < lines.length &&
152+
!(lines[nextIndex]?.includes("}'") || lines[nextIndex]?.includes("]'"))
153+
) {
154+
currentBuffer += '\n' + lines[nextIndex];
155+
nextIndex++;
156+
}
157+
158+
// Add the final line with the closing quote if found
159+
if (nextIndex < lines.length) {
160+
currentBuffer += '\n' + lines[nextIndex];
161+
nextIndex++;
162+
}
163+
}
164+
165+
return { updatedBuffer: currentBuffer, updatedIndex: nextIndex - 1 };
166+
}
167+
135168
/**
136169
* Parses a block of text into a `VulnerableApiFinding` object.
137170
* @param block - The text block to parse.
@@ -148,25 +181,48 @@ function parseVulnerableApiFindingBlock(block: string): VulnerableApiFinding {
148181
// Process the first line separately to handle initial URL or description
149182
processFirstLine(lines, finding);
150183

151-
lines.forEach((line) => {
184+
for (let i = 0; i < lines.length; i++) {
185+
const line = lines[i] || '';
186+
const trimmedLine = line.trim();
152187
const parsedLine = parseLine(line);
153188

154189
if (parsedLine) {
155190
const [key, value] = parsedLine;
156191

157192
if (currentKey && currentBuffer) {
158-
// If a previous key exists, update it with the accumulated buffer
193+
// Update the previous key with the accumulated buffer
159194
updateFindingField(finding, currentKey, currentBuffer, currentSection);
195+
160196
currentBuffer = null; // Reset buffer after updating
161197
currentKey = null; // Reset key after updating
162198
}
163199

164200
if (key) {
165-
if (currentKey && currentBuffer) {
166-
// Continue accumulating the value if the same key is detected
167-
currentBuffer += `\n${value}`;
201+
if (key === 'body') {
202+
// Special handling for body field
203+
const { updatedBuffer, updatedIndex } = handleBodyField(
204+
key,
205+
value,
206+
lines,
207+
i
208+
);
209+
210+
currentBuffer = updatedBuffer;
211+
i = updatedIndex;
212+
currentKey = key;
213+
214+
const { updatedSection } = updateVulnerableApiFinding(
215+
finding,
216+
key,
217+
currentBuffer,
218+
currentSection
219+
);
220+
221+
if (updatedSection !== currentSection) {
222+
currentSection = updatedSection;
223+
}
168224
} else {
169-
// If a new key is detected, update section and headers
225+
// Normal key-value handling
170226
const { updatedSection } = updateVulnerableApiFinding(
171227
finding,
172228
key,
@@ -184,10 +240,14 @@ function parseVulnerableApiFindingBlock(block: string): VulnerableApiFinding {
184240
}
185241
}
186242
} else if (currentBuffer) {
187-
// Continue accumulating the value if no new key is detected
188-
currentBuffer += `\n${line}`;
243+
// Handle normal line continuations (non-body content)
244+
if (line.match(/^\s{2,}/)) {
245+
currentBuffer += ' ' + trimmedLine;
246+
} else {
247+
currentBuffer += '\n' + line;
248+
}
189249
}
190-
});
250+
}
191251

192252
// Finalize any remaining buffer
193253
if (currentBuffer && currentKey) {

tests/unit/utils/parse-vulnerable-api-finding-test.js

+94
Original file line numberDiff line numberDiff line change
@@ -222,10 +222,104 @@ module('Unit | Utility | parse-vulnerable-api-finding', function (hooks) {
222222
},
223223
];
224224

225+
const content4 =
226+
'mobile.useinsider.com:443/api/v3/session/start: The time elapsed between the sending of the request and the arrival of the response exceeds the expected amount of time, suggesting a vulnerability to command injection attacks.\nconfidence: MEDIUM\nparam:\n location: headers\n method: POST\n variables:\n - Accept\n - Content-Type\n - Accept-Language\n - Host\n - Content-Length\n - Connection\n - Accept-Encoding\n - User-Agent\n - Ts\nrequest:\n body: \'{"insider_id": "53B4EFCC2CFD40AAA5BF73B303F90BEB", "device_info": {"location_enabled":\n false, "app_version": "5.6.0", "push_enabled": false, "os_version": "16.7.7",\n "battery": 100, "sdk_version": "13.0.0", "connection": "wifi"}, "partner_name":\n "nbdliv", "calledDueToInsiderIdChange": false, "first_run": true, "udid": "53B4EFCC2CFD40AAA5BF73B303F90BEB"}\'\n headers:\n Accept: \'*/*\'\n Accept-Encoding: gzip, deflate, br\n Ts: \'1717657521\'\n User-Agent: Liv/666 CFNetwork/1410.1 Darwin/22.6.0\n method: POST\n params: {}\n url: https://mobile.useinsider.com:443/api/v3/session/start\nresponse:\n cookies:\n __cf_bm: Y344vnTG2lq7wTlqEMFGsTIQ3PlYUFLpHkTrqTVlEzI-1717659521-1.0.1.1-mYaqUeGWUgRuX6qX3QSTYeBDLGEiWy7QvsPT.m.V81ZZla2e22iXwUZND2QPqL4Cv2r0yvmQuigDf0_Y_dAx8A\n headers:\n CF-RAY: 88f6be8aaa872258-ORD\n Cache-Control: private,\n Vary: Accept-Encoding\n X-Frame-Options: SAMEORIGIN\n reason: Forbidden\n status_code: 403\n text: "\\uFFFD(\\uFFFD\\0 \\uFFFDM"';
227+
228+
const expectedObject4 = [
229+
{
230+
confidence: 'MEDIUM',
231+
description:
232+
'The time elapsed between the sending of the request and the arrival of the response exceeds the expected amount of time, suggesting a vulnerability to command injection attacks.',
233+
request: {
234+
body: '\'{"insider_id": "53B4EFCC2CFD40AAA5BF73B303F90BEB", "device_info": {"location_enabled":\n false, "app_version": "5.6.0", "push_enabled": false, "os_version": "16.7.7",\n "battery": 100, "sdk_version": "13.0.0", "connection": "wifi"}, "partner_name":\n "nbdliv", "calledDueToInsiderIdChange": false, "first_run": true, "udid": "53B4EFCC2CFD40AAA5BF73B303F90BEB"}\'',
235+
cookies: {},
236+
headers: {
237+
accept: "'*/*'",
238+
'accept-encoding': 'gzip, deflate, br',
239+
ts: "'1717657521'",
240+
'user-agent': 'Liv/666 CFNetwork/1410.1 Darwin/22.6.0',
241+
},
242+
method: 'POST',
243+
params: {},
244+
url: 'https://mobile.useinsider.com:443/api/v3/session/start',
245+
},
246+
response: {
247+
cookies: {
248+
__cf_bm:
249+
'Y344vnTG2lq7wTlqEMFGsTIQ3PlYUFLpHkTrqTVlEzI-1717659521-1.0.1.1-mYaqUeGWUgRuX6qX3QSTYeBDLGEiWy7QvsPT.m.V81ZZla2e22iXwUZND2QPqL4Cv2r0yvmQuigDf0_Y_dAx8A',
250+
},
251+
headers: {
252+
'cache-control': 'private,',
253+
'cf-ray': '88f6be8aaa872258-ORD',
254+
vary: 'Accept-Encoding',
255+
'x-frame-options': 'SAMEORIGIN',
256+
},
257+
reason: 'Forbidden',
258+
status_code: 403,
259+
text: '"\\uFFFD(\\uFFFD\\0 \\uFFFDM"',
260+
url: '',
261+
},
262+
severity: '',
263+
url: 'mobile.useinsider.com:443/api/v3/session/start',
264+
},
265+
];
266+
267+
const content5 =
268+
'mobile-collector.newrelic.com:443/mobile/v3/data: The difference in length between the response to the baseline request and the request returned when sending an attack string exceeds 1000.0 percent, which could indicate a vulnerability to injection attacks\nconfidence: LOW\nparam:\n location: headers\n method: POST\n variables:\n - Connection\n - Content-Length\n - Content-Type\n - User-Agent\n - Content-Encoding\n - Accept-Encoding\n - X-Newrelic-Connect-Time\nrequest:\n body: \'[[482998014, 594496047], ["Android", "10", "Mi A2", "AndroidAgent", "6.9.0",\n "b36f41b0-37ae-4a68-9a54-d860c6876323", "", "", "Xiaomi", {"size": "normal", "platform":\n "Native", "platformVersion": "6.9.0"}], 0.0, [], [[{"scope": "", "name": "Memory/Used"},\n {"count": 1, "total": 76.3740234375, "min": 76.3740234375, "max": 76.3740234375,\n "sum_of_squares": 5832.991456031799}]], [], [], [], {}, []]\'\n headers:\n Accept-Encoding: gzip\n Connection: Keep-Alive\n Content-Encoding: identity\n Content-Length: \'358\'\n Content-Type: application/json\n Host: mobile-collector.newrelic.com:443\n User-Agent: Dalvik/2.1.0 (Linux; U; Android 10; Mi A2 Build/QQ3A.200805.001)\n X-App-License-Key: AAa728a7f147e1cf95a25a315203d656a36f602257-NRMA\n X-Newrelic-Connect-Time: \'*!@#$^&()[]{}|.,"\\\'\'/\'\'\'\'"\'\n method: POST\n params: {}\n url: https://mobile-collector.newrelic.com:443/mobile/v3/data\nresponse:\n cookies: {}\n headers:\n CF-Cache-Status: DYNAMIC\n CF-Ray: 89e502ccbbcbc5cb-ORD\n Connection: keep-alive\n Content-Length: \'2\'\n Content-Type: application/json; charset=UTF-8\n Date: Fri, 05 Jul 2024 05:38:48 GMT\n Server: cloudflare\n Vary: Accept-Encoding\n reason: OK\n status_code: 200\n text: \'{}\'';
269+
270+
const expectedObject5 = [
271+
{
272+
confidence: 'LOW',
273+
description:
274+
'The difference in length between the response to the baseline request and the request returned when sending an attack string exceeds 1000.0 percent, which could indicate a vulnerability to injection attacks',
275+
request: {
276+
body: '\'[[482998014, 594496047], ["Android", "10", "Mi A2", "AndroidAgent", "6.9.0",\n "b36f41b0-37ae-4a68-9a54-d860c6876323", "", "", "Xiaomi", {"size": "normal", "platform":\n "Native", "platformVersion": "6.9.0"}], 0.0, [], [[{"scope": "", "name": "Memory/Used"},\n {"count": 1, "total": 76.3740234375, "min": 76.3740234375, "max": 76.3740234375,\n "sum_of_squares": 5832.991456031799}]], [], [], [], {}, []]\'',
277+
cookies: {},
278+
headers: {
279+
'accept-encoding': 'gzip',
280+
connection: 'Keep-Alive',
281+
'content-encoding': 'identity',
282+
'content-length': "'358'",
283+
'content-type': 'application/json',
284+
host: 'mobile-collector.newrelic.com:443',
285+
'user-agent':
286+
'Dalvik/2.1.0 (Linux; U; Android 10; Mi A2 Build/QQ3A.200805.001)',
287+
'x-app-license-key':
288+
'AAa728a7f147e1cf95a25a315203d656a36f602257-NRMA',
289+
'x-newrelic-connect-time': "'*!@#$^&()[]{}|.,\"\\''/''''\"'",
290+
},
291+
method: 'POST',
292+
params: {},
293+
url: 'https://mobile-collector.newrelic.com:443/mobile/v3/data',
294+
},
295+
response: {
296+
cookies: {},
297+
headers: {
298+
'cf-cache-status': 'DYNAMIC',
299+
'cf-ray': '89e502ccbbcbc5cb-ORD',
300+
connection: 'keep-alive',
301+
'content-length': "'2'",
302+
'content-type': 'application/json; charset=UTF-8',
303+
date: 'Fri, 05 Jul 2024 05:38:48 GMT',
304+
server: 'cloudflare',
305+
vary: 'Accept-Encoding',
306+
},
307+
reason: 'OK',
308+
status_code: 200,
309+
text: "'{}'",
310+
url: '',
311+
},
312+
severity: '',
313+
url: 'mobile-collector.newrelic.com:443/mobile/v3/data',
314+
},
315+
];
316+
225317
const vulnerableApiFindings = [
226318
{ content: content1, expectedObject: expectedObject1 },
227319
{ content: content2, expectedObject: expectedObject2 },
228320
{ content: content3, expectedObject: expectedObject3 },
321+
{ content: content4, expectedObject: expectedObject4 },
322+
{ content: content5, expectedObject: expectedObject5 },
229323
];
230324

231325
test.each(

0 commit comments

Comments
 (0)