├── Burp-MissingScannerChecks.py └── README.md /Burp-MissingScannerChecks.py: -------------------------------------------------------------------------------- 1 | # Burp Additonal Scanner Checks Extension 2 | # Copyright 2017 Thomas Patzke 3 | # 4 | # Parts of this code (DOMXSS REs) are based on work licensed under LGPL 5 | # and can be found here: 6 | # https://code.google.com/p/domxsswiki/wiki/FindingDOMXSS 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program. If not, see . 20 | 21 | from burp import (IBurpExtender, IScannerCheck, IScanIssue, ITab) 22 | from javax.swing import (GroupLayout, JPanel, JCheckBox, JTextField, JLabel, JButton) 23 | from array import array 24 | import re 25 | import pickle 26 | 27 | STSMinimum = 60 * 60 * 24 * 90 # Minimum for Strict Transport Security: 90 days 28 | issueTypeDOMXSS = 2097930 29 | issueNameDOMXSS = "Possible DOM-based Cross-site scripting" 30 | issueTypeSTS = 5245380 31 | issueNameSTS = "Strict Transport Security Misconfiguration" 32 | issueTypeXCTO = 8389890 33 | issueNameXCTO = "Content Sniffing not disabled" 34 | issueTypeRedirectFromHTTP2HTTPS = 5244500 35 | issueNameRedirectFromHTTP2HTTPS = "Redirection from HTTP to HTTPS" 36 | issueTypeXXP = 5245361 37 | issueNameXXP = "Browser cross-site scripting filter misconfiguration" 38 | 39 | class BurpExtender(IBurpExtender, IScannerCheck, IScanIssue, ITab): 40 | def registerExtenderCallbacks(self, callbacks): 41 | self.callbacks = callbacks 42 | self.helpers = callbacks.getHelpers() 43 | callbacks.setExtensionName("Missing Scanner Checks") 44 | self.out = callbacks.getStdout() 45 | 46 | # define all checkboxes 47 | self.cbPassiveChecks = self.defineCheckBox("Passive Scanner Checks") 48 | self.cbDOMXSS = self.defineCheckBox("DOM XSS", False) 49 | self.cbDOMXSSSources = self.defineCheckBox("Sources", False) 50 | self.cbDOMXSSSinks = self.defineCheckBox("Sinks") 51 | self.cbDOMXSSjQuerySinks = self.defineCheckBox("jQuery Sinks", False) 52 | self.grpDOMXSSSettings = JPanel() 53 | self.grpDOMXSSSettings.add(self.cbDOMXSSSources) 54 | self.grpDOMXSSSettings.add(self.cbDOMXSSSinks) 55 | self.grpDOMXSSSettings.add(self.cbDOMXSSjQuerySinks) 56 | self.cbSTS = self.defineCheckBox("Strict Transport Security") 57 | self.lblSTSMin = JLabel("Minimum acceptable max-age") 58 | self.inSTSMin = JTextField(str(STSMinimum), 9, actionPerformed=self.setSTSMinimum) # TODO: actionPerformed only fires on enter key - focus lost would be better 59 | self.inSTSMin.setToolTipText("Enter the minimum max-age value which is considered as acceptable. Press return to change setting!") 60 | self.grpSTSSettings = JPanel() 61 | self.grpSTSSettings.add(self.lblSTSMin) 62 | self.grpSTSSettings.add(self.inSTSMin) 63 | self.cbXCTO = self.defineCheckBox("Content Sniffing") 64 | self.cbXXP = self.defineCheckBox("Client-side XSS Filter Configuration") 65 | self.cbRedirToHTTPS = self.defineCheckBox("Redirection from HTTP to HTTPS") 66 | self.btnSave = JButton("Set as default", actionPerformed=self.saveConfig) 67 | self.btnRestore = JButton("Restore", actionPerformed=self.restoreConfig) 68 | self.grpConfig = JPanel() 69 | self.grpConfig.add(self.btnSave) 70 | self.grpConfig.add(self.btnRestore) 71 | self.restoreConfig() 72 | 73 | # definition of config tab 74 | self.tab = JPanel() 75 | layout = GroupLayout(self.tab) 76 | self.tab.setLayout(layout) 77 | layout.setAutoCreateGaps(True) 78 | layout.setAutoCreateContainerGaps(True) 79 | layout.setHorizontalGroup( 80 | layout.createSequentialGroup() 81 | .addGroup(layout.createParallelGroup() 82 | .addComponent(self.cbPassiveChecks) 83 | ) 84 | .addGroup(layout.createParallelGroup() 85 | .addComponent(self.cbDOMXSS) 86 | .addComponent(self.cbSTS) 87 | .addComponent(self.cbXCTO) 88 | .addComponent(self.cbXXP) 89 | .addComponent(self.cbRedirToHTTPS) 90 | ) 91 | .addGroup(layout.createParallelGroup() 92 | .addComponent(self.grpDOMXSSSettings, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE) 93 | .addComponent(self.grpSTSSettings, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE) 94 | .addComponent(self.grpConfig, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE) 95 | ) 96 | ) 97 | layout.setVerticalGroup( 98 | layout.createSequentialGroup() 99 | .addGroup(layout.createParallelGroup() 100 | .addComponent(self.cbPassiveChecks) 101 | .addComponent(self.cbDOMXSS) 102 | .addComponent(self.grpDOMXSSSettings, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE) 103 | ) 104 | .addGroup(layout.createParallelGroup() 105 | .addComponent(self.cbSTS) 106 | .addComponent(self.grpSTSSettings, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE) 107 | ) 108 | .addComponent(self.cbXCTO) 109 | .addComponent(self.cbXXP) 110 | .addComponent(self.cbRedirToHTTPS) 111 | .addComponent(self.grpConfig, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE) 112 | ) 113 | 114 | self.domXSSSourcesRE = re.compile("(location\s*[\[.])|([.\[]\s*[\"']?\s*(arguments|dialogArguments|innerHTML|write(ln)?|open(Dialog)?|showModalDialog|cookie|URL|documentURI|baseURI|referrer|name|opener|parent|top|content|self|frames)\W)|(localStorage|sessionStorage|Database)") 115 | # NOTE: done some optimizations here, original RE caused too much noise 116 | # - added leading dot in the first part - original recognized " 0: # One of the DOMXSS REs matched 211 | scanIssues.append(DOMXSSScanIssue( 212 | baseRequestResponse, 213 | domXSSSourcesPos, 214 | domXSSSinksPos, 215 | domXSSjQuerySinksPos, 216 | self.helpers, 217 | self.callbacks 218 | )) 219 | 220 | # Identify missing, wrong or multiple occurring HTTP headers 221 | headersSTS = list() 222 | headersXCTO = list() 223 | headersXXP = list() 224 | headersLocationHTTPS = list() 225 | 226 | offset = 0 227 | for header in responseHeaders: 228 | if self.cbSTS.isSelected(): 229 | match = self.headerSTSRE.match(header) 230 | if match: 231 | headersSTS.append((match, offset)) 232 | 233 | if self.cbXCTO.isSelected(): 234 | match = self.headerXCTORE.match(header) 235 | if match: 236 | headersXCTO.append(match) 237 | 238 | if self.cbXXP.isSelected(): 239 | match = self.headerXXP.match(header) 240 | if match: 241 | headersXXP.append((match, offset)) 242 | 243 | if self.cbRedirToHTTPS.isSelected() and requestProtocol == 'http': 244 | match = self.headerLocationHTTPS.match(header) 245 | if match: 246 | headersLocationHTTPS.append((match, offset)) 247 | 248 | offset += len(header) + 2 # TODO: assumption that CRLF is always used. The world is ugly, make a real check. 249 | 250 | if requestProtocol != "https": 251 | pass # HSTS only valid in HTTPS responses. 252 | elif self.cbSTS.isSelected(): 253 | if len(headersSTS) == 0: # No HSTS header 254 | scanIssues.append(STSScanIssue( 255 | baseRequestResponse, 256 | STSScanIssue.caseNoHeader, 257 | None, 258 | self.helpers, 259 | self.callbacks 260 | )) 261 | elif len(headersSTS) == 1 and int(headersSTS[0][0].group(1)) < STSMinimum: # HSTS header present, but time frame too short 262 | scanIssues.append(STSScanIssue( 263 | baseRequestResponse, 264 | STSScanIssue.caseTooLow, 265 | (int(headersSTS[0][0].group(1)), headersSTS[0][1] + headersSTS[0][0].start(1), headersSTS[0][1] + headersSTS[0][0].end(1)), 266 | self.helpers, 267 | self.callbacks 268 | )) 269 | elif len(headersSTS) > 1: # multiple HSTS headers 270 | scanIssues.append(STSScanIssue( 271 | baseRequestResponse, 272 | STSScanIssue.caseMultipleHeaders, 273 | headersSTS, 274 | self.helpers, 275 | self.callbacks 276 | )) 277 | 278 | # Redirection from HTTP to HTTPS 279 | if self.cbRedirToHTTPS.isSelected() and len(headersLocationHTTPS) > 0: 280 | scanIssues.append(RedirectFromHTTP2HTTPSScanIssue( 281 | baseRequestResponse, 282 | headersLocationHTTPS, 283 | self.helpers, 284 | self.callbacks 285 | )) 286 | 287 | if self.cbXXP.isSelected(): 288 | if len(headersXXP) == 0: # No XSS protection header 289 | scanIssues.append(XXPScanIssue( 290 | baseRequestResponse, 291 | XXPScanIssue.caseNoHeader, 292 | None, 293 | self.helpers, 294 | self.callbacks 295 | )) 296 | elif len(headersXXP) == 1 and int(headersXXP[0][0].group(1)) == 1 and headersXXP[0][0].group(2) != "block": # Activated but not in block mode 297 | scanIssues.append(XXPScanIssue( 298 | baseRequestResponse, 299 | XXPScanIssue.caseNoBlockMode, 300 | headersXXP, 301 | self.helpers, 302 | self.callbacks 303 | )) 304 | elif len(headersXXP) > 1: # Multiple XXP headers 305 | scanIssues.append(XXPScanIssue( 306 | baseRequestResponse, 307 | XXPScanIssue.caseMultipleHeaders, 308 | headersXXP, 309 | self.helpers, 310 | self.callbacks 311 | )) 312 | # "X-XSS-Protection: 0" already catched by Burp 313 | 314 | 315 | # X-Content-Type-Options missing 316 | # NOTE: it is assumed that multiple "X-Content-Type-Options: nosniff" headers can't cause confusion at browser side because they all have the same meaning. 317 | if self.cbXCTO.isSelected() and len(headersXCTO) == 0: 318 | scanIssues.append(XCTOScanIssue( 319 | baseRequestResponse, 320 | self.helpers, 321 | self.callbacks 322 | )) 323 | 324 | return scanIssues 325 | 326 | def consolidateDuplicateIssues(self, existingIssue, newIssue): 327 | if existingIssue.getIssueName() == newIssue.getIssueName(): 328 | if newIssue.getIssueName() == issueNameDOMXSS: # DOMXSS issues are different if response content is different. 329 | responseExisting = existingIssue.getHttpMessages()[0].getResponse() 330 | analyzedResponseExisting = self.helpers.analyzeResponse(responseExisting) 331 | bodyOffsetExisting = analyzedResponseExisting.getBodyOffset() 332 | responseBodyExisting = responseExisting.getResponse()[analyzedResponseExisting.getBodyOffset():].tostring() 333 | 334 | responseNew = newIssue.getHttpMessages()[0].getResponse() 335 | analyzedResponseNew = self.helpers.analyzeResponse(responseNew) 336 | bodyOffsetNew = analyzedResponseNew.getBodyOffset() 337 | responseBodyNew = responseNew.getResponse()[analyzedResponseNew.getBodyOffset():].tostring() 338 | 339 | if responseBodyExisting == responseBodyNew: 340 | return -1 341 | else: 342 | return 0 343 | elif newIssue.getIssueName() == issueNameRedirectFromHTTP2HTTPS: # Redirection issues are different if target URLs differ 344 | if existingIssue.getIssueDetail() == newIssue.getIssueDetail(): 345 | return -1 346 | else: 347 | return 0 348 | else: # In all other cases: keep existing issue 349 | return -1 350 | return 0 351 | 352 | class DOMXSSScanIssue(IScanIssue): 353 | def __init__(self, requestResponse, sourcesPos, sinksPos, jQuerySinksPos, helpers, callbacks): 354 | analyzedRequest = helpers.analyzeRequest(requestResponse) 355 | self.findingUrl = analyzedRequest.getUrl() 356 | self.sourcesPos = sourcesPos 357 | self.sinksPos = sinksPos + jQuerySinksPos 358 | self.requestResponse = callbacks.applyMarkers(requestResponse, None, normalizePositions(self.sourcesPos + self.sinksPos)) 359 | 360 | def getUrl(self): 361 | return self.findingUrl 362 | 363 | def getIssueName(self): 364 | return issueNameDOMXSS 365 | 366 | def getIssueType(self): 367 | return issueTypeDOMXSS 368 | 369 | def getSeverity(self): 370 | if len(self.sinksPos) > 0: 371 | return "High" 372 | else: 373 | return "Low" 374 | 375 | def getConfidence(self): 376 | return "Tentative" 377 | 378 | def getIssueBackground(self): 379 | msg = "

DOM-based cross-site scripting (XSS) is a variant of the well-known XSS vulnerabilities where the issue is located in client-side JavaScript code. \ 380 | As in classical XSS, the vulnerability causes code execution in the context of the users session within the application. An attacker can perform a wide \ 381 | variety of actions, like session hijacking etc. The vulnerability is caused by copying data from non-trustworthy sources (user input, URL) into the DOM with \ 382 | insecure methods.

\ 383 |

Such vulnerabilities are hard to detect with classical methods like checking the response for occurrences of previous inputs or parameters. This scan \ 384 | issue has detected the usage of insecure sources or sinks in the JavaScript code of the page. Further manual checks must be performed to verify the impact \ 385 | of their usage.

" 386 | return msg 387 | 388 | def getRemediationBackground(self): 389 | msg = "DOM-based XSS can be prevented by usage of secure methods that only operate on the content text of the DOM instead of modifying the DOM structure. E.g. \ 390 | usage of the innerText attribute instead of innerHTML or the text() method of jQuery instead of html() to insert user content into the DOM. If these methods are \ 391 | not applicable in a particular use case, the user input has to be filtered and sanitized before it is passed into the DOM by insecure methods." 392 | return msg 393 | 394 | def getIssueDetail(self): 395 | msg = "The scanner check has detected occurrences of unsafe " 396 | if len(self.sinksPos) > 0 and len(self.sourcesPos) > 0: 397 | msg += "sources and sinks. If data flows from unsafe sources into unsafe sinks without being sanitized then it is quite certain that the web application is \ 398 | vulnerable against DOM-based XSS. " 399 | elif len(self.sinksPos) > 0: 400 | msg += "sinks. Be aware that there are potential unsafe sources which are not detected by the patterns used in the scanner check, e.g. data loaded by the \ 401 | XmlHttpRequest API. Furthermore there is the possibility that the sources and sinks are distributed in different files. " 402 | elif len(self.sourcesPos) > 0: 403 | msg += "sources. Generally there must be a sink for code execution caused by unsafe sources. This could be located in a different part of the web application \ 404 | or be unrecognized by the scanner module. " 405 | msg += "See the response tab to review the occurrences." 406 | return msg 407 | 408 | def getRemediationDetail(self): 409 | return None 410 | 411 | def getHttpMessages(self): 412 | return [self.requestResponse] 413 | 414 | def getHttpService(self): 415 | return self.requestResponse.getHttpService() 416 | 417 | 418 | class STSScanIssue(IScanIssue): 419 | caseNoHeader = 1 420 | caseTooLow = 2 421 | caseMultipleHeaders = 3 422 | 423 | def __init__(self, requestResponse, case, data, helpers, callbacks): 424 | analyzedRequest = helpers.analyzeRequest(requestResponse) 425 | self.findingUrl = analyzedRequest.getUrl() 426 | self.case = case 427 | self.data = data 428 | self.STSMinimum = STSMinimum 429 | if case == self.caseNoHeader: 430 | self.requestResponse = requestResponse 431 | elif case == self.caseTooLow: 432 | self.requestResponse = callbacks.applyMarkers(requestResponse, None, [array('i', (data[1], data[2]))]) 433 | elif case == self.caseMultipleHeaders: 434 | self.requestResponse = callbacks.applyMarkers(requestResponse, None, normalizePositions(extractMatchPositions(data))) 435 | 436 | def getUrl(self): 437 | return self.findingUrl 438 | 439 | def getIssueName(self): 440 | return issueNameSTS 441 | 442 | def getIssueType(self): 443 | return issueTypeSTS 444 | 445 | def getSeverity(self): 446 | return "Medium" 447 | 448 | def getConfidence(self): 449 | return "Certain" 450 | 451 | def getIssueBackground(self): 452 | return "

The HTTP Strict Transport Security policy defines a timeframe where a browser must connect to the web server via HTTPS. Without a Strict Transport \ 453 | Security policy the web application may be vulnerable against several attacks:

\ 454 |
    \ 455 |
  • If the web application mixes usage of HTTP and HTTPS, an attacker can manipulate pages in the unsecured area of the application or change redirection targets \ 456 | in a manner that the switch to the secured page is not performed or done in a manner, that the attacker remains between client and server.
  • \ 457 |
  • If there is no HTTP server, an attacker in the same network could simulate a HTTP server and motivate the user to click on a prepared URL by a scoial \ 458 | engineering attack.
  • \ 459 |
\ 460 |

The protection is effective only for the given amount of time. Multiple occurrence of this header could cause undefined behaviour in browsers and should be avoided.

" 461 | return msg 462 | 463 | def getRemediationBackground(self): 464 | return None 465 | 466 | def getIssueDetail(self): 467 | msg = None 468 | if self.case == self.caseNoHeader: 469 | msg = "There was no \"Strict-Transport-Security\" header in the server response." 470 | elif self.case == self.caseTooLow: 471 | msg = "A \"Strict-Transport-Security\" header was set in the server response and the time frame was set to " + str(self.data[0]) + ". This is considered as too low." 472 | elif self.case == self.caseMultipleHeaders: 473 | msg = "Multiple occurrences of the \"Strict-Transport-Security\" header were seen in the HTTP response. This could cause undefined behaviour with browsers, \ 474 | because it is unclear, which header is used." 475 | return msg 476 | 477 | def getRemediationDetail(self): 478 | msg = None 479 | if self.case == self.caseNoHeader: 480 | msg = "

A Strict-Transport-Security HTTP header should be sent with each HTTPS response. The syntax is as follows:

\ 481 |

Strict-Transport-Security: max-age=<seconds>[; includeSubDomains]

\ 482 |

The parameter max-age gives the time frame for requirement of HTTPS in seconds and should be chosen quite high, e.g. several months.\ 483 | A value below " + str(self.STSMinimum) + " is considered as too low by this scanner check. \ 484 | The flag includeSubDomains defines that the policy applies also for sub domains of the sender of the response.

" 485 | elif self.case == self.caseTooLow: 486 | msg = "The given time frame should be increased to a minimum of " + str(self.STSMinimum) + " seconds." 487 | elif self.case == self.caseMultipleHeaders: 488 | msg = "There should be only one header defining a strict transport security policy. The Time frame should be set at minimum to " + str(self.STSMinimum) + "." 489 | return msg 490 | 491 | def getHttpMessages(self): 492 | return [self.requestResponse] 493 | 494 | def getHttpService(self): 495 | return self.requestResponse.getHttpService() 496 | 497 | 498 | class RedirectFromHTTP2HTTPSScanIssue(IScanIssue): 499 | def __init__(self, requestResponse, headers, helpers, callbacks): 500 | analyzedRequest = helpers.analyzeRequest(requestResponse) 501 | self.findingUrl = analyzedRequest.getUrl() 502 | self.redirectURLs = map(lambda(header): header[0].group(1), headers) 503 | self.redirectURLs.sort() 504 | self.requestResponse = callbacks.applyMarkers(requestResponse, None, normalizePositions(extractMatchPositions(headers))) 505 | 506 | def getUrl(self): 507 | return self.findingUrl 508 | 509 | def getIssueName(self): 510 | return issueNameRedirectFromHTTP2HTTPS 511 | 512 | def getIssueType(self): 513 | return issueTypeRedirectFromHTTP2HTTPS 514 | 515 | def getSeverity(self): 516 | return "Medium" 517 | 518 | def getConfidence(self): 519 | return "Certain" 520 | 521 | def getIssueBackground(self): 522 | msg = "The redirection to a HTTPS URL is transmitted over the insecure HTTP protocol. This makes the redirection itself vulnerable against Man-in-the-Middle attacks. \ 523 | An attacker could redirect the user to a slightly different HTTPS URL which is under his control or keep the connection unencrypted by stripping down to HTTP and relaying \ 524 | between client and server." 525 | return msg 526 | 527 | def getRemediationBackground(self): 528 | msg = "

Usage of HTTP should be kept at a minimum in web applications where security matters. Users which enter the web application via HTTP, e.g. by entering only the domain name in the \ 529 | URL bar of their browser should be redirected directly to a secure HTTPS URL. All HTTPS resources should provide a Strict-Transport-Security header which ensures that the \ 530 | browser uses only HTTPS for a given amount of time. The syntax for this header is as follows:

\ 531 |

Strict-Transport-Security: max-age=<seconds>[; includeSubDomains]

\ 532 |

The parameter max-age gives the time frame for requirement of HTTPS in seconds and should be chosen quite high, e.g. several months. Except the initial redirection the \ 533 | application should be used completely with HTTPS.

" 534 | return msg 535 | 536 | def getIssueDetail(self): 537 | msg = "The web application redirects the browser from HTTP to the following HTTPS URL" 538 | if len(self.redirectURLs) == 1: 539 | msg += ": " + self.redirectURLs[0] + "" 540 | else: 541 | msg += "s:
    " 542 | for redirectURL in self.redirectURLs: 543 | msg += "
  • " + redirectURL + "
  • " 544 | msg += "

Multiple headers were given. The redirection target depends on the used browser.

" 545 | return msg 546 | 547 | def getRemediationDetail(self): 548 | return None 549 | 550 | def getHttpMessages(self): 551 | return [self.requestResponse] 552 | 553 | def getHttpService(self): 554 | return self.requestResponse.getHttpService() 555 | 556 | 557 | class XXPScanIssue(IScanIssue): 558 | caseNoHeader = 1 559 | caseNoBlockMode = 2 560 | caseMultipleHeaders = 3 561 | 562 | def __init__(self, requestResponse, case, headers, helpers, callbacks): 563 | analyzedRequest = helpers.analyzeRequest(requestResponse) 564 | self.findingUrl = analyzedRequest.getUrl() 565 | self.case = case 566 | self.headers = headers 567 | if case == self.caseNoHeader: 568 | self.requestResponse = requestResponse 569 | elif case == self.caseNoBlockMode: 570 | self.requestResponse = callbacks.applyMarkers(requestResponse, None, [array('i', (headers[0][1], headers[0][1] + headers[0][0].end(0)))]) 571 | elif case == self.caseMultipleHeaders: 572 | self.requestResponse = callbacks.applyMarkers(requestResponse, None, normalizePositions(extractMatchPositions(headers))) 573 | 574 | def getUrl(self): 575 | return self.findingUrl 576 | 577 | def getIssueName(self): 578 | return issueNameXXP 579 | 580 | def getIssueType(self): 581 | return issueTypeXXP 582 | 583 | def getSeverity(self): 584 | return "Low" 585 | 586 | def getConfidence(self): 587 | return "Certain" 588 | 589 | def getIssueBackground(self): 590 | return "Cross-site scripting (XSS) filters in browsers check if the URL contains possible harmful XSS payloads and if they are reflected in the response page. If \ 591 | such a condition is recognized, the injected code is changed in a way, that it is not executed anymore to prevent a succesful XSS attack. The downside of these filters \ 592 | is, that the browser has no possibility to distinguish between code fragments which were reflected by a vulnerable web application in an XSS attack and these which are \ 593 | already present on the page. In the past, these filters were used by attackers to deactivate JavaScript code on the attacked web page. Sometimes the XSS filters itself are \ 594 | vulnerable in a way, that web applications which were protected properly against XSS attacks became vulnerable under certain conditions." 595 | 596 | def getRemediationBackground(self): 597 | return "It is considered as better practice to instruct the browser XSS filter to never render the web page if an XSS attack is detected." 598 | 599 | def getIssueDetail(self): 600 | msg = None 601 | if self.case == self.caseNoHeader: 602 | msg = "No X-XSS-Protection header was set in the response. This means that the browser uses default behaviour that detection of a cross-site scripting attack never prevents rendering." 603 | elif self.case == self.caseNoBlockMode: 604 | msg = "A X-XSS-Protection header is set and XSS protection is enabled, but blocking mode is not set." 605 | elif self.case == self.caseMultipleHeaders: 606 | msg = "Multiple occurrences of the \"X-XSS-Protection\" header were seen in the HTTP response. This could cause undefined behaviour with browsers, \ 607 | because it is unclear, which header is used." 608 | return msg 609 | 610 | def getRemediationDetail(self): 611 | msg = None 612 | if self.case == self.caseNoHeader: 613 | msg = "

The following header should be set:

\ 614 |

X-XSS-Protection: 1; mode=block

" 615 | elif self.case == self.caseNoBlockMode: 616 | msg = "

Set the option \"mode=block\" in the X-XSS-Protection HTTP header as follows:

\ 617 |

X-XSS-Protection: 1; mode=block

" 618 | elif self.case == self.caseMultipleHeaders: 619 | msg = "There should be only one X-XSS-Protection header present." 620 | return msg 621 | 622 | def getHttpMessages(self): 623 | return [self.requestResponse] 624 | 625 | def getHttpService(self): 626 | return self.requestResponse.getHttpService() 627 | 628 | 629 | class XCTOScanIssue(IScanIssue): 630 | def __init__(self, requestResponse, helpers, callbacks): 631 | analyzedRequest = helpers.analyzeRequest(requestResponse) 632 | self.findingUrl = analyzedRequest.getUrl() 633 | self.requestResponse = requestResponse 634 | 635 | def getUrl(self): 636 | return self.findingUrl 637 | 638 | def getIssueName(self): 639 | return issueNameXCTO 640 | 641 | def getIssueType(self): 642 | return issueTypeXCTO 643 | 644 | def getSeverity(self): 645 | return "Low" 646 | 647 | def getConfidence(self): 648 | return "Certain" 649 | 650 | def getIssueBackground(self): 651 | msg = "There was no \"X-Content-Type-Options\" HTTP header with the value nosniff set in the response. The lack of this header causes that certain browsers, \ 652 | try to determine the content type and encoding of the response even when these properties are defined correctly. This can make the web application \ 653 | vulnerable against Cross-Site Scripting (XSS) attacks. E.g. the Internet Explorer and Safari treat responses with the content type text/plain as HTML, if they contain \ 654 | HTML tags." 655 | return msg 656 | 657 | def getRemediationBackground(self): 658 | msg = "Set the following HTTP header at least in all responses which contain user input:
X-Content-Type-Options: nosniff
" 659 | return msg 660 | 661 | def getIssueDetail(self): 662 | return None 663 | 664 | def getRemediationDetail(self): 665 | return None 666 | 667 | def getHttpMessages(self): 668 | return [self.requestResponse] 669 | 670 | def getHttpService(self): 671 | return self.requestResponse.getHttpService() 672 | 673 | 674 | ### Helpers ### 675 | # Sort and merge overlapping match ranges - needed by Burp for scan issue markers 676 | def normalizePositions(uPos): 677 | sortedPos = sorted(uPos) 678 | normPos = list() 679 | prevPos = None 680 | for pos in sortedPos: 681 | if not prevPos: 682 | prevPos = pos 683 | elif prevPos[1] > pos[0]: 684 | if prevPos[1] < pos[1]: 685 | prevPos[1] = pos[1] 686 | else: 687 | normPos.append(prevPos) 688 | prevPos = pos 689 | normPos.append(prevPos) 690 | return normPos 691 | 692 | # extract match positions from an array of matches as expected by Burp for scan issue markers 693 | def extractMatchPositions(matches, bodyOffset = 0): 694 | try: 695 | if isinstance(matches, list) and isinstance(matches[0], tuple): 696 | return map(lambda(match, offset): array('i', (match.start() + bodyOffset + offset, match.end() + bodyOffset + offset)), matches) 697 | else: 698 | return map(lambda(match): array('i', (match.start() + bodyOffset, match.end() + bodyOffset)), matches) 699 | except IndexError: 700 | return list() 701 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IMPORTANT: Archived Project 2 | 3 | This project is not maintained anymore, please fork and do changes on your own. 4 | 5 | # Burp Missing Scanner Checks 6 | 7 | This burp extension implements some passive scanner checks which are 8 | missing in Burp suite: 9 | 10 | * DOM-based XSS (REs are based on those from https://code.google.com/p/domxsswiki/wiki/FindingDOMXSS) 11 | 12 | * Missing HTTP headers: 13 | 14 | * Strict-Transport-Security 15 | 16 | * X-Content-Type-Options: nosniff 17 | 18 | * X-XSS-Protection 19 | 20 | * Multiple occurrences of the checked headers. 21 | 22 | * Redirection from HTTP to HTTPS 23 | 24 | All checks can be enabled separately in an own extension tab and a default config can be stored. 25 | 26 | ## TODO 27 | 28 | * See TODO markers in the code. 29 | 30 | * Further possibilities to redirect from HTTP to HTTPS (meta refresh, links, referer checking) 31 | 32 | * Active scanner check: Actively test directories for listings 33 | 34 | * Active scanner check: Add parameters like debug, admin, test etc. and check if something 35 | interesting appears on the page. 36 | 37 | * Active Scanner check: Reaction of the web application and server to 38 | requests with different/missing host headers. 39 | --------------------------------------------------------------------------------