Skip to content

Commit

Permalink
add html formatting support in request response findings
Browse files Browse the repository at this point in the history
  • Loading branch information
avzz-19 committed Nov 6, 2024
1 parent 11c3dbc commit f1bc06a
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface FileDetailsVulnerabilityAnalysisDetailsFindingsVulnerableApiSig
interface FormattedResult {
value: string;
isJSON?: boolean;
isHTML?: boolean;
}

interface VulnerabilityDetails {
Expand Down Expand Up @@ -140,37 +141,67 @@ export default class FileDetailsVulnerabilityAnalysisDetailsFindingsVulnerableAp
.replace(/(^['"])|(['"]$)/g, '')
.replace(/\\n/g, '\n');

// Check if it's HTML content
if (
sanitizedString.includes('<!DOCTYPE html') ||
sanitizedString.includes('<html') ||
sanitizedString.toLowerCase().includes('<!doctype html')
) {
return {
value: sanitizedString,
isJSON: false,
isHTML: true,
};
}

// Try to parse as JSON first
try {
const parsed = JSON.parse(sanitizedString);

return {
value: JSON.stringify(parsed, null, 2),
isJSON: true,
isHTML: false,
};
} catch {
// If JSON parsing fails, return the sanitized string
return {
value: sanitizedString,
isJSON: false,
isHTML: false,
};
}
}

getWhiteSpaceStyle(formattedBody: {
isJSON?: boolean;
isHTML?: boolean;
}): string {
return formattedBody.isJSON || formattedBody.isHTML
? 'pre-wrap'
: 'pre-line';
}

get vulnerabilityDetails() {
const request = this.args.currentVulnerability?.request;
const response = this.args.currentVulnerability?.response;

const formattedRequestBody = this.getFormattedText(request?.body);
const formattedResponseBody = this.getFormattedText(response?.text);

const reqBodyWhitespaceStyle =
this.getWhiteSpaceStyle(formattedRequestBody);

const resBodyWhitespaceStyle = this.getWhiteSpaceStyle(
formattedResponseBody
);

return [
{
title: this.intl.t('requestBody'),
value: formattedRequestBody.value,
isEmpty: this.isRequestBodyEmpty,
copyIcon: true,
whiteSpace: formattedRequestBody.isJSON ? 'pre-wrap' : 'pre-line',
whiteSpace: reqBodyWhitespaceStyle,
},
{
title: this.intl.t('requestHeaders'),
Expand Down Expand Up @@ -217,7 +248,7 @@ export default class FileDetailsVulnerabilityAnalysisDetailsFindingsVulnerableAp
title: this.intl.t('responseBody'),
value: formattedResponseBody.value,
isEmpty: this.isResponseBodyEmpty,
whiteSpace: formattedResponseBody.isJSON ? 'pre-wrap' : 'pre-line',
whiteSpace: resBodyWhitespaceStyle,
copyIcon: true,
},
] as VulnerabilityDetails[];
Expand Down
118 changes: 96 additions & 22 deletions app/utils/parse-vulnerable-api-finding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,47 @@ function splitVulnerableApiFindingIntoBlocks(content: string): string[] {
return content.split(/\n{2,3}/);
}

function formatHtmlResponseBody(html: string): string {
// Remove escaped quotes and newlines
let formatted = html
.replace(/\\"/g, '"')
.replace(/\\\n/g, '')
.replace(/\\ /g, '');

// Add newlines after closing tags
formatted = formatted.replace(/>/g, '>\n');

// Add indentation
const lines = formatted.split('\n');
let indent = 0;
const indentSize = 2;

return lines
.map((line) => {
line = line.trim();
if (!line) {
return '';
}

// Decrease indent for closing tags
if (line.match(/^<\//)) {
indent -= indentSize;
}

// Add current indentation
const currentIndent = ' '.repeat(Math.max(0, indent));

// Increase indent for opening tags, except self-closing
if (line.match(/^<[^/]/) && !line.match(/\/>/)) {
indent += indentSize;
}

return currentIndent + line;
})
.filter((line) => line)
.join('\n');
}

function handleBodyField(
key: string,
value: string,
Expand All @@ -141,8 +182,16 @@ function handleBodyField(
let currentBuffer = value;
let nextIndex = i + 1;

// Check if it's a multi-line content
if (value.includes('\\r\\n') || value.includes('\\n')) {
// Handle error messages with line breaks
currentBuffer = value
.replace(/\\\s+/g, '') // Remove escaped spaces
.replace(/\\r\\n/g, '\n')
.replace(/\\n/g, '\n');
}
// Check if body value is a multi-line JSON or array
if (
else if (
(value.startsWith("'{") && !value.endsWith("}'")) ||
(value.startsWith("'[") && !value.endsWith("]'"))
) {
Expand All @@ -154,15 +203,35 @@ function handleBodyField(
currentBuffer += '\n' + lines[nextIndex];
nextIndex++;
}

// Add the final line with the closing quote if found
if (nextIndex < lines.length) {
currentBuffer += '\n' + lines[nextIndex];
nextIndex++;
}
}
// Handle HTML content
else if (
currentBuffer.includes('"<!DOCTYPE html') ||
currentBuffer.includes('"<html') ||
currentBuffer.toLowerCase().includes('"<!doctype html')
) {
while (nextIndex < lines.length && !lines[nextIndex]?.includes('</html>')) {
currentBuffer += '\n' + lines[nextIndex];
nextIndex++;
}
// Add the final line with the closing quote if found
if (nextIndex < lines.length) {
currentBuffer += '\n' + lines[nextIndex];
nextIndex++;
}

currentBuffer = formatHtmlResponseBody(currentBuffer);
}

return { updatedBuffer: currentBuffer, updatedIndex: nextIndex - 1 };
return {
updatedBuffer: currentBuffer,
updatedIndex: nextIndex - 1,
};
}

/**
Expand All @@ -177,29 +246,30 @@ function parseVulnerableApiFindingBlock(block: string): VulnerableApiFinding {
let currentSection: VulnerabilityApiFindingSection = 'confidence';
let currentBuffer: string | null = null;
let currentKey: string | null = null;
let isCollectingMultiLine = false;

// Process the first line separately to handle initial URL or description
processFirstLine(lines, finding);

for (let i = 0; i < lines.length; i++) {
const line = lines[i] || '';
const trimmedLine = line.trim();
const parsedLine = parseLine(line);
const parsedLine = !isCollectingMultiLine ? parseLine(line) : null;

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

if (currentKey && currentBuffer) {
if (currentKey && currentBuffer && !isCollectingMultiLine) {
// Update the previous key with the accumulated buffer
updateFindingField(finding, currentKey, currentBuffer, currentSection);

currentBuffer = null; // Reset buffer after updating
currentKey = null; // Reset key after updating
currentBuffer = null;
currentKey = null;
}

if (key) {
if (key === 'body') {
// Special handling for body field
if (key === 'body' || key === 'text') {
// Special handling for body/text field
isCollectingMultiLine = true;
const { updatedBuffer, updatedIndex } = handleBodyField(
key,
value,
Expand All @@ -211,6 +281,7 @@ function parseVulnerableApiFindingBlock(block: string): VulnerableApiFinding {
i = updatedIndex;
currentKey = key;

// Process the multi-line content
const { updatedSection } = updateVulnerableApiFinding(
finding,
key,
Expand All @@ -221,6 +292,8 @@ function parseVulnerableApiFindingBlock(block: string): VulnerableApiFinding {
if (updatedSection !== currentSection) {
currentSection = updatedSection;
}

isCollectingMultiLine = false;
} else {
// Normal key-value handling
const { updatedSection } = updateVulnerableApiFinding(
Expand All @@ -240,8 +313,12 @@ function parseVulnerableApiFindingBlock(block: string): VulnerableApiFinding {
}
}
} else if (currentBuffer) {
// Handle normal line continuations (non-body content)
if (line.match(/^\s{2,}/)) {
// Handle line continuations
if (isCollectingMultiLine) {
// For text/body fields, preserve original formatting
currentBuffer += '\n' + line;
} else if (line.match(/^\s{2,}/)) {
// For other fields, handle indented continuations
currentBuffer += ' ' + trimmedLine;
} else {
currentBuffer += '\n' + line;
Expand All @@ -263,23 +340,20 @@ function parseVulnerableApiFindingBlock(block: string): VulnerableApiFinding {
* @returns A tuple containing the key and value as strings, or `null` if the line cannot be parsed.
*/
function parseLine(line: string): [string, string] | null {
const [key, ...valueParts] = line.split(':');

if (!key) {
const colonIndex = line.indexOf(':');
if (colonIndex === -1) {
return null;
}

// Split the line into key and value parts based on the first colon
const colonIndex = line.indexOf(':');

// If there's no colon, return null as it's not a valid key-value pair
if (colonIndex === -1) {
// Check if this colon is part of "://"
if (line.substring(colonIndex, colonIndex + 3) === '://') {
return null;
}

const value = valueParts.join(':').trim();
const key = line.substring(0, colonIndex).trim().toLowerCase();
const value = line.substring(colonIndex + 1).trim();

return [key.trim().toLowerCase(), value];
return key ? [key, value] : null;
}

/**
Expand Down
10 changes: 5 additions & 5 deletions tests/unit/utils/parse-vulnerable-api-finding-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ module('Unit | Utility | parse-vulnerable-api-finding', function (hooks) {
const expectedObject3 = [
{
request: {
body: '\'{"operationName": "GetAdhocTasks", "variables": {"driverId": 3, "startTime": "2024-09-02 14:00:00", "endTime": "2024-09-03 00:00:00"}, "query": "query GetAdhocTasks($driverId: Int, $endTime: String, $startTime: String) {\\n adhocTasks(driverId: $driverId, endTime: $endTime, startTime: $startTime) {\\n adhoctaskId\\n driverId\\n note\\n startTime\\n endDeadline\\n issueType\\n delayedTask\\n __typename\\n }\\n}"}\'',
body: '\'{"operationName": "GetAdhocTasks", "variables": {"driverId": 3, "startTime": "2024-09-02 14:00:00", "endTime": "2024-09-03 00:00:00"}, "query": "query GetAdhocTasks($driverId: Int, $endTime: String, $startTime: String) {\n adhocTasks(driverId: $driverId, endTime: $endTime, startTime: $startTime) {\n adhoctaskId\n driverId\n note\n startTime\n endDeadline\n issueType\n delayedTask\n __typename\n }\n}"}\'',
cookies: {},
headers: {
accept: "'*/*'",
Expand Down Expand Up @@ -265,7 +265,7 @@ module('Unit | Utility | parse-vulnerable-api-finding', function (hooks) {
];

const content5 =
'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: \'{}\'';
'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: "<!DOCTYPE html><html lang=\\"en\\" id=\\"facebook\\"><head><title>Error</title><meta\\\n \\ charset=\\"utf-8\\" /><meta http-equiv=\\"Cache-Control\\" content=\\"no-cache\\"\\\n \\ /><meta name=\\"robots\\" content=\\"noindex,nofollow\\" /><style nonce=\\"wfWasHre\\"\\\n >html, body { color: #333; font-family: \\\'Lucida Grande\\\', \\\'Tahoma\\\', \\\'Verdana\\\',\\\n \\\'Arial\\\', sans-serif; margin: 0; padding: 0; text-align: center;}\\n#header {\\\n \\ height: 30px; padding-bottom: 10px; padding-top: 10px; text-align: center;}\\n\\\n #icon { width: 30px;}\\n.core { margin: auto; padding: 1em 0; text-align: left;\\\n \\ width: 904px;}\\nh1 { font-size: 18px;}\\np { font-size: 13px;}\\n.footer { border-top:\\\n \\ 1px solid #ddd; color: #777; float: left; font-size: 11px; padding: 5px 8px\\\n \\ 6px 0; width: 904px;}</style></head><body><div id=\\"header\\"><a href=\\"//www.facebook.com/\\"\\\n ><img id=\\"icon\\" src=\\"//static.facebook.com/images/logos/facebook_2x.png\\" /></a></div><div\\\n \\ class=\\"core\\"><h1>Sorry, something went wrong.</h1><p>We&#039;re working on\\\n \\ getting this fixed as soon as we can.</p><p><a id=\\"back\\" href=\\"//www.facebook.com/\\"\\\n >Go back</a></p><div class=\\"footer\\"> Meta &#169; 2024 &#183; <a href=\\"//www.facebook.com/help/?ref=href052\\"\\\n >Help</a></div></div><script nonce=\\"wfWasHre\\">\\n document.getElementById(\\"\\\n back\\").onclick = function() {\\n if (history.length > 1) {\\n \\\n \\ history.back();\\n return false;\\n \\\n \\ }\\n };\\n </script></body></html>"\n url: https://graph.facebook.com:443/oauth/token\n version: 11\nseverity: HIGH\n\n\n';

const expectedObject5 = [
{
Expand Down Expand Up @@ -306,10 +306,10 @@ module('Unit | Utility | parse-vulnerable-api-finding', function (hooks) {
},
reason: 'OK',
status_code: 200,
text: "'{}'",
url: '',
text: '"<!DOCTYPE html>\n<html lang="en" id="facebook">\n <head>\n <title>\n Error</title>\n <meta charset="utf-8" />\n <meta http-equiv="Cache-Control" content="no-cache" />\n <meta name="robots" content="noindex,nofollow" />\n <style nonce="wfWasHre" >\n html, body { color: #333; font-family: \\\'Lucida Grande\\\', \\\'Tahoma\\\', \\\'Verdana\\\', \\\'Arial\\\', sans-serif; margin: 0; padding: 0; text-align: center;}\\n#header { height: 30px; padding-bottom: 10px; padding-top: 10px; text-align: center;}\\n #icon { width: 30px;}\\n.core { margin: auto; padding: 1em 0; text-align: left; width: 904px;}\\nh1 { font-size: 18px;}\\np { font-size: 13px;}\\n.footer { border-top: 1px solid #ddd; color: #777; float: left; font-size: 11px; padding: 5px 8px 6px 0; width: 904px;}</style>\n </head>\n <body>\n <div id="header">\n <a href="//www.facebook.com/" >\n <img id="icon" src="//static.facebook.com/images/logos/facebook_2x.png" />\n </a>\n </div>\n <div class="core">\n <h1>\n Sorry, something went wrong.</h1>\n <p>\n We&#039;re working on getting this fixed as soon as we can.</p>\n <p>\n <a id="back" href="//www.facebook.com/" >\n Go back</a>\n </p>\n <div class="footer">\n Meta &#169; 2024 &#183; <a href="//www.facebook.com/help/?ref=href052" >\n Help</a>\n </div>\n </div>\n <script nonce="wfWasHre">\n \\n document.getElementById(" back").onclick = function() {\\n if (history.length >\n 1) {\\n history.back();\\n return false;\\n }\\n };\\n </script>\n </body>\n </html>\n "',
url: 'https://graph.facebook.com:443/oauth/token',
},
severity: '',
severity: 'HIGH',
url: 'mobile-collector.newrelic.com:443/mobile/v3/data',
},
];
Expand Down

0 comments on commit f1bc06a

Please sign in to comment.