Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
xwiki-platform-core/xwiki-platform-xclass/xwiki-platform-xclass-ui/src/main/resources/XWiki/ClassSheet.xml
### 代码内容：
<?xml version="1.1" encoding="UTF-8"?>

<xwikidoc version="1.5" reference="XWiki.ClassSheet" locale="">
  <web>XWiki</web>
  <name>ClassSheet</name>
  <language/>
  <defaultLanguage/>
  <translation>0</translation>
  <creator>xwiki:XWiki.Admin</creator>
  <parent>XWiki.XWikiClasses</parent>
  <author>xwiki:XWiki.Admin</author>
  <contentAuthor>xwiki:XWiki.Admin</contentAuthor>
  <version>1.1</version>
  <title>#sheetTitle('platform.xclass.defaultClassSheet.title')</title>
  <comment/>
  <minorEdit>false</minorEdit>
  <syntaxId>xwiki/2.0</syntaxId>
  <hidden>true</hidden>
  <content>{{template name="locationPicker_macros.vm" /}}

{{velocity}}
## This document can be copied in order to be customized so we cannot rely on its name to determine if the currently
## displayed document is a class or the class sheet itself. We look for the sheet descriptor instead.
#set ($isSheet = $doc.getObject('XWiki.SheetDescriptorClass'))
#if ($isSheet)
  ## Viewing the sheet document itself.
  {{translation key="platform.xclass.defaultClassSheet.description"/}}
#elseif ("$!request.bindSheet" != '' && $hasEdit)
  #if ($services.csrf.isTokenValid($request.getParameter('form_token')))
    ## Bind the sheet to the class.
    #set ($classSheetReference = $services.model.resolveDocument($request.bindSheet))
    #if ($services.sheet.bindClassSheet($doc, $classSheetReference))
      $doc.save($services.localization.render('platform.xclass.defaultClassSheet.sheets.bind'))
    #end
    $response.sendRedirect($request.xredirect)
  #else
    $response.sendRedirect($services.csrf.getResubmissionURL())
  #end
  ## Stop processing, since we already sent a redirect.
  #stop
#elseif("$!request.docName" != '')
  ## Request for creating a new instance.
  ## We don't actually create a new instance here, we just redirect to the edit mode.
  #set ($targetSpaceRef = $services.model.resolveSpace($request.spaceName))
  #set ($targetDocRef = $services.model.createDocumentReference($request.docName, $targetSpaceRef))
  #if (!$xwiki.exists($targetDocRef) && $services.security.authorization.hasAccess('edit', $targetDocRef))
    ## Compute the default edit mode to ensure backward compatibility with documents that are still using the deprecated
    ## inline action.
    #set ($editAction = $xwiki.getDocument($request.template).getDefaultEditMode())
    $response.sendRedirect($xwiki.getURL($targetDocRef, $editAction, $escapetool.url({
      'template': $request.template,
      'parent': $request.parent,
      'title': $request.docName
    })))
    ## Stop processing, since we already sent a redirect.
    #stop
  #end
#end
{{/velocity}}

{{velocity}}
## If this sheet is explicitly bound to the displayed class then print the class document content before the
## sheet output. Class authors can put the description of the class in the class document content.
#set($classSheetReference = $services.model.createDocumentReference($doc.wiki, 'XWiki', 'ClassSheet'))
#if($services.sheet.getDocumentSheets($doc).contains($classSheetReference))
  {{include reference="" /}}
#end
{{/velocity}}

{{velocity}}
#if (!$isSheet)
  #set ($className = $doc.pageReference.name)
  #set ($className = $stringtool.removeEnd($className, 'Class'))
  ## Determine the class sheets.
  #set ($classSheetReferences = $services.sheet.getClassSheets($doc))
  #if ($classSheetReferences.isEmpty())
    ## There is no class sheet explicitly bound to this class. Fall-back on naming convention.
    ## Before XWiki 2.0, the default class sheet was suffixed with "ClassSheet". Since 2.0, the suffix is just "Sheet".
    #set ($defaultClassSheetReference = $services.model.createDocumentReference("${className}ClassSheet",
      $doc.documentReference.parent))
    #if (!$xwiki.exists($defaultClassSheetReference))
      #set ($defaultClassSheetReference = $services.model.createDocumentReference("${className}Sheet",
        $doc.documentReference.parent))
    #end
  #end
  ## Determine the template using naming convention.
  ## Before XWiki 2.0, the default class template was suffixed with "ClassTemplate".
  ## Since 2.0, the suffix is just "Template".
  #set ($classTemplateReference = $services.model.createDocumentReference("${className}ClassTemplate",
    $doc.documentReference.parent))
  #if (!$xwiki.exists($classTemplateReference))
    #set ($classTemplateReference = $services.model.createDocumentReference("${className}Template",
      $doc.documentReference.parent))
  #end
  ## Determine the template provider using naming convention.
  #set ($classTemplateProviderReference = $services.model.createDocumentReference("${className}TemplateProvider",
    $doc.documentReference.parent))
  #set ($classTemplateProviderDoc = $xwiki.getDocument($classTemplateProviderReference))
  #set ($hasClassTemplateProvider = !$classTemplateProviderDoc.isNew())
  #set($classTemplateDoc = $xwiki.getDocument($classTemplateReference))
  #set($hasClassSheets = !$classSheetReferences.isEmpty() || $xwiki.exists($defaultClassSheetReference))
  #set($hasClassTemplate = !$classTemplateDoc.isNew())
  #if(!$defaultSpace)
    #set($defaultSpace = $doc.space)
  #end
  #if(!$defaultParent)
    #set($defaultParent = ${doc.fullName})
  #end

  #set ($classEditorURL = $doc.getURL('edit', 'editor=class'))
  #if($doc.getxWikiClass().properties.size() == 0)
    #set ($openLink = "")
    #set ($closeLink = '')
    {{warning}}
      {{html}}
      ## First escape the content of the translation, then replace the placeholders with content that would otherwise be
      ## escaped during the first escaping.
      #set ($warningMessage = $services.localization.render('platform.xclass.defaultClassSheet.properties.empty',
        ['__OPEN_LINK__', '__CLOSE_LINK__']))
      $escapetool.xml($warningMessage).replace('__OPEN_LINK__', $openLink).replace('__CLOSE_LINK__', $closeLink)
      {{/html}}
    {{/warning}}
  #else
    (% id="HClassProperties" %)
    = {{translation key="platform.xclass.defaultClassSheet.properties.heading"/}} =
    #foreach($property in $doc.getxWikiClass().properties)
      * $services.rendering.escape("$property.prettyName ($property.name: $xwiki.metaclass.get($property.classType).prettyName)", $xwiki.currentContentSyntaxId)
    #end
    #set ($openLink = "")
    #set ($closeLink = '')
    #set ($warningMessage = $escapetool.xml($services.localization.render('platform.xclass.defaultClassSheet.properties.edit', ['__OPEN_LINK__', '__CLOSE_LINK__'])))
    ## First escape the content of the translation, then replace the placeholders with content that would otherwise be
    ## escaped during the first escaping.
    * //{{html}}$warningMessage.replace('__OPEN_LINK__', $openLink).replace('__CLOSE_LINK__', $closeLink){{/html}}//

  #end
  #if ($hasClassSheets && $hasClassTemplate)
    (% id="HCreatePage" %)
    = {{translation key="platform.xclass.defaultClassSheet.createPage.heading"/}} =
    #if("$!targetDocRef" != '' && $xwiki.exists($targetDocRef))
      {{warning}}
        {{html}}
          #set ($targetDocLink = $xwiki.getURL($targetDocRef))
          #set ($openLink = "")
          #set ($message = $escapetool.xml($services.localization.render('platform.xclass.defaultClassSheet.createPage.pageAlreadyExists', ['__OPEN_LINK__', '__CLOSE_LINK__'])))
          ## First escape the content of the translation, then replace the placeholders with content that would
          ## otherwise be escaped during the first escaping.
          $message.replace('__OPEN_LINK__', $openLink).replace('__CLOSE_LINK__', '')
        {{/html}}
      {{/warning}}
    #elseif("$!targetDocRef" != '')

      {{warning}}{{translation key="platform.xclass.defaultClassSheet.createPage.denied"/}}{{/warning}}
    #end

    {{html}}
    <form action="$doc.getURL()" id="newdoc" method="post" class="xform half">
      <fieldset>

        <input type="hidden" name="form_token" value="$!{services.csrf.getToken()}" />
        <input type="hidden" name="parent" value="$escapetool.xml(${defaultParent})"/>
        <input type="hidden" name="template" value="$escapetool.xml(${classTemplateDoc})"/>
        <input type="hidden" name="sheet" value="1"/>

      #locationPicker({
        'id': 'target',
        'title': {
          'label': 'core.create.title',
          'hint': 'core.create.title.hint',
          'name': 'docTitle',
          'placeholder': 'core.create.name.placeholder'
        },
        'preview': {
          'label': 'core.create.locationPreview.label',
          'hint': 'core.create.locationPreview.hint'
        },
        'parent': {
          'label': 'core.create.spaceReference.label',
          'hint': 'core.create.spaceReference.hint',
          'name': 'spaceName',
          'reference': $services.model.resolveSpace($defaultSpace),
          'placeholder': 'core.create.spaceReference.placeholder'
        },
        'name': {
          'label': 'core.create.name.label',
          'hint': 'core.create.name.hint',
          'name': 'docName',
          'placeholder': 'core.create.name.placeholder'
        }
      })

          <input type="submit" class="button" value="$escapetool.xml($services.localization.render(
            'platform.xclass.defaultClassSheet.createPage.label'))"/>

      </fieldset>
    </form>
    {{/html}}

  #end## has class sheet and class template
  (% id="HExistingPages" %)
  = {{translation key="platform.xclass.defaultClassSheet.pages.heading"/}} =

  {{translation key="platform.xclass.defaultClassSheet.pages.description"/}}

  #set ($options = {
    'className': $doc.fullName,
    'translationPrefix' : 'platform.index.',
    'queryFilters': ['unique']
  })
  {{liveData
    id="classEntries"
    properties="doc.title,doc.location,doc.date,doc.author,doc.objectCount,_actions"
    source="liveTable"
    className="$services.rendering.escape(${doc.fullName}, 'xwiki/2.1')"
    sourceParameters="$services.rendering.escape($escapetool.url($options), 'xwiki/2.1')"
  }}
  {
    "meta": {
      "propertyDescriptors": [
        {
          "id": "doc.title",
          "editable": false
        },
        {
          "id": "doc.objectCount",
          "editable": false,
          "filterable": false,
          "sortable": false
        }
      ]
    }
  }
  {{/liveData}}

  (% id="HClassSheets" %)
  = {{translation key="platform.xclass.defaultClassSheet.sheets.heading"/}} =
  #if (!$hasClassSheets || !$hasClassTemplate)

    {{translation key="platform.xclass.defaultClassSheet.sheets.missing"/}}
  #end

  {{info}}
    #set ($message = $services.localization.render('platform.xclass.defaultClassSheet.sheets.description', ['__START_EM__', '__END_EM__']))
    #set ($message = $escapetool.xml($message))
    ## First escape the content of the translation, then replace the placeholders with content that would
    ## otherwise be escaped during the first escaping.
    {{html}}$message.replace('__START_EM__', '').replace('__END_EM__', ''){{/html}}
  {{/info}}

  #if(!$hasClassSheets)
    {{html}}
      <form action="$xwiki.getURL($defaultClassSheetReference, 'save', 'editor=wiki')" method="post">

          <input type="hidden" name="form_token" value="$!{services.csrf.getToken()}" />
          <input type="hidden" name="parent" value="$escapetool.xml(${doc.fullName})"/>
          <input type="hidden" name="xredirect" value="$escapetool.xml(${doc.URL})"/>
          #set ($sheetContent = $xwiki.getDocument('XWiki.ObjectSheet').getContent().replace('XWiki.MyClass',
            $doc.fullName))
          ## We have to encode the new line characters in order to preserve them, otherwise they are replace with a
          ## space when the HTML is cleaned.
          ## FIXME: Use a dedicated escape tool method when XCOMMONS-405 is implemented.
          #set ($sheetContent = $escapetool.xml($sheetContent).replaceAll("\n", '&#10;'))
          <input type="hidden" name="content" value="$sheetContent"/>
          <input type="hidden" name="title" value="${escapetool.h}if(${escapetool.d}doc.documentReference.name == '$escapetool.xml($defaultClassSheetReference.name)')$escapetool.xml($className) Sheet${escapetool.h}{else}${escapetool.d}services.display.title(${escapetool.d}doc, {'displayerHint': 'default', 'outputSyntaxId': 'plain/1.0'})${escapetool.h}end"/>
          <input type="submit" class="button" value="$escapetool.xml(
            $services.localization.render('platform.xclass.defaultClassSheet.sheets.create'))"/>

      </form>
    {{/html}}
  #else
    #set($defaultClassSheetDoc = $xwiki.getDocument($defaultClassSheetReference))
    #if($classSheetReferences.isEmpty() && !$defaultClassSheetDoc.getObject('XWiki.SheetClass'))
      ## The sheet is not bound to the class.
      #set($xredirect = $xwiki.relativeRequestURL)
      #set($defaultClassSheetStringReference = $services.model.serialize($defaultClassSheetReference, "default"))
      #set($bindURL = $doc.getURL('view', "bindSheet=${escapetool.url($defaultClassSheetStringReference)}&xredirect=${escapetool.url($xredirect)}&form_token=$!{services.csrf.getToken()}"))
      {{warning}}
        {{translation key="platform.xclass.defaultClassSheet.sheets.notBound"/}} ##
        #if ($hasEdit)
          {{html}}
          ##
            $escapetool.xml($services.localization.render('platform.xclass.defaultClassSheet.sheets.bind')) »##
          .
          {{/html}}
        #end
      {{/warning}}

    #end
    #if ($classSheetReferences.size() < 2)
      #set($classSheetDoc = $defaultClassSheetDoc)
      #if(!$classSheetReferences.isEmpty())
        #set($classSheetDoc = $xwiki.getDocument($classSheetReferences.get(0)))
      #end
      #set ($sheetPath = "#hierarchy($classSheetDoc.documentReference, {'plain': true, 'local': true, 'limit': 4})")
      #set ($classSheetLink = "$services.localization.render('platform.xclass.defaultClassSheet.sheets.view', [$sheetPath.trim()]) »")
      #set ($classSheetLink = $services.rendering.escape($classSheetLink, 'xwiki/2.1'))
      #set ($classSheetLink = $services.rendering.escape($classSheetLink, 'xwiki/2.1'))
      #set ($classSheetText = ${classSheetDoc.fullName})
      #set ($classSheetText = $services.rendering.escape($classSheetText, 'xwiki/2.1'))
      [[$classSheetLink>>$classSheetText]]
    #else
      {{translation key="platform.xclass.defaultClassSheet.sheets.list"/}}

      #foreach($classSheetReference in $classSheetReferences)
        * [[$services.model.serialize($classSheetReference, "default")]]
      #end
    #end
  #end

  (% id="HClassTemplate" %)
  = {{translation key="platform.xclass.defaultClassSheet.template.heading"/}} =

    {{info}}
      #set ($message = $services.localization.render('platform.xclass.defaultClassSheet.template.description', ['__START_EM__', '__END_EM__']))
      #set ($message = $escapetool.xml($message))
      ## First escape the content of the translation, then replace the placeholders with content that would
      ## otherwise be escaped during the first escaping.
      {{html}}$message.replace('__START_EM__', '').replace('__END_EM__', ''){{/html}}
    {{/info}}

  #if (!$hasClassTemplate)
    {{html}}
      <form action="$escapetool.xml($classTemplateDoc.getURL('save', 'editor=wiki'))" method="post">

          <input type="hidden" name="form_token" value="$!{services.csrf.getToken()}" />
          <input type="hidden" name="parent" value="$escapetool.xml(${doc.fullName})"/>
          <input type="hidden" name="xredirect" value="$escapetool.xml(${doc.URL})"/>
          <input type="hidden" name="title" value="$escapetool.xml($className) Template"/>
          <input type="submit" class="button" value="$escapetool.xml(
            $services.localization.render('platform.xclass.defaultClassSheet.template.create'))"/>

      </form>
    {{/html}}
  #else
    #if(!$classTemplateDoc.getObject(${doc.fullName}))
      #set($xredirect = $xwiki.relativeRequestURL)
      #set($createUrl = $classTemplateDoc.getURL('objectadd', "classname=${escapetool.url($doc.fullName)}&xredirect=${escapetool.url($xredirect)}&form_token=$!{services.csrf.getToken()}"))
      {{warning}}
        #set ($message = $services.localization.render('platform.xclass.defaultClassSheet.template.missingObject', ['__CLASS_NAME__']))
        #set ($message = $escapetool.xml($message))
        {{html}}
          ## First escape the content of the translation, then replace the placeholders with content that would
          ## otherwise be escaped during the first escaping.
          $message.replace('__CLASS_NAME__', "$escapetool.xml($className)")
          ##
            $escapetool.xml($services.localization.render('platform.xclass.defaultClassSheet.template.addObject', [$className])) »##
          .
        {{/html}}
      {{/warning}}

    #end
    #set ($templatePath = "#hierarchy($classTemplateDoc.documentReference, {'plain': true, 'local': true, 'limit': 4})")
    #set ($templateDocLink = "$services.localization.render('platform.xclass.defaultClassSheet.template.view', [$templatePath.trim()]) »")
    #set ($templateDocLink = $services.rendering.escape($templateDocLink, 'xwiki/2.1'))
    #set ($templateDocLink = $services.rendering.escape($templateDocLink, 'xwiki/2.1'))
    #set ($templateDocText = "${classTemplateDoc.fullName}")
    ## First escape the xwiki/2.1 syntax of the translation, then replace the placeholders with content that would
    ## otherwise be escaped during the first escaping.
    #set ($templateDocText = $services.rendering.escape($templateDocText, 'xwiki/2.1'))
    [[$templateDocLink>>$templateDocText]]
  #end
  ## Create a template provider only if a template for the current class exists.
  #if ($classTemplateDoc.getObject(${doc.fullName}))
    (% id="HClassTemplateProvider" %)
    = {{translation key="platform.xclass.defaultClassSheet.templateProvider.heading"/}} =

      {{info}}
        #set ($message = $services.localization.render('platform.xclass.defaultClassSheet.templateProvider.description', ['__EM__']))
        #set ($message = $services.rendering.escape($message, 'xwiki/2.1'))
        ## First escape the xwiki/2.1 syntax of the translation, then replace the placeholders with content that would
        ## otherwise be escaped during the first escaping.
        ## The replacement key is itself escaped, and it's escaped form needs to be used for the replacement.
        $message.replace('~_~_~E~M~_~_', '//')
      {{/info}}

    #if (!$hasClassTemplateProvider)
      #set ($templateProviderClassName = 'XWiki.TemplateProviderClass')
      ## Do the page creation and object addition in one step, providing some default values.
      ## In order to get the root space of the class and use it as restrictionSpace, we need to be sure that we have
      ## the expected result for multiple level hierarchies, like MyApplication.Code.MyApplicationClass. In this case,
      ## the template provider in enabled in MyApplication space.
      #set ($restrictionSpace = $doc.documentReference.spaceReferences.get(0).name)
      #set ($createUrlQueryString = $escapetool.url({
        'classname': $templateProviderClassName,
        'xredirect': $xwiki.relativeRequestURL,
        'form_token': $services.csrf.token,
        "${templateProviderClassName}_name": $className,
        "${templateProviderClassName}_description":
          $services.localization.render('platform.xclass.templateProvider.defaultDescription', [$className]),
        "${templateProviderClassName}_template": $classTemplateDoc,
        "${templateProviderClassName}_visibilityRestrictions": $restrictionSpace}))
      #set ($createUrl = $classTemplateProviderDoc.getURL('objectadd', $createUrlQueryString))
      {{html}}
        <form action="$escapetool.xml($classTemplateProviderDoc.getURL('save', 'editor=wiki'))" method="post">

            <input type="hidden" name="form_token" value="$!{services.csrf.getToken()}" />
            <input type="hidden" name="parent" value="$escapetool.xml(${doc.fullName})"/>
            <input type="hidden" name="xredirect" value="$escapetool.xml($createUrl)"/>
            <input type="hidden" name="title" value="$escapetool.xml($className) Template Provider"/>
            <input type="submit" class="button" value="$escapetool.xml(
              $services.localization.render('platform.xclass.defaultClassSheet.templateProvider.create'))"/>

        </form>
      {{/html}}
    #else
      #set ($templateProviderPath = "#hierarchy($classTemplateProviderDoc.documentReference, {'plain': true, 'local': true, 'limit': 4})")
      #set ($linkTarget = "$services.localization.render('platform.xclass.defaultClassSheet.templateProvider.view', [$templateProviderPath.trim()]) »")
      #set ($linkTarget = $services.rendering.escape($linkTarget, 'xwiki/2.1'))
      #set ($linkTarget = $services.rendering.escape($linkTarget, 'xwiki/2.1'))
      #set ($linkLabel = $services.rendering.escape(${classTemplateProviderDoc.fullName}, 'xwiki/2.1'))
      [[$linkTarget>>$linkLabel]]
    #end
  #end

#end## !$isSheet
{{/velocity}}</content>
  <object>
    <name>XWiki.ClassSheet</name>
    <number>0</number>
    <className>XWiki.SheetDescriptorClass</className>
    <guid>4d854769-efce-441c-8594-349c458b880e</guid>
    <class>
      <name>XWiki.SheetDescriptorClass</name>
      <customClass/>
      <customMapping/>
      <defaultViewSheet/>
      <defaultEditSheet/>
      <defaultWeb/>
      <nameField/>
      <validationScript/>
      <action>
        <customDisplay/>
        <disabled>0</disabled>
        <name>action</name>
        <number>1</number>
        <picker>0</picker>
        <prettyName>Action</prettyName>
        <size>30</size>
        <unmodifiable>0</unmodifiable>
        <validationMessage/>
        <validationRegExp/>
        <classType>com.xpn.xwiki.objects.classes.StringClass</classType>
      </action>
    </class>
    <property>
      <action>view</action>
    </property>
  </object>
</xwikidoc>

Output:
【漏洞类型】路径遍历漏洞
【严重性】High
【漏洞摘要】在原始代码中，`{{include reference="" /}}` 指令用于包含其他文档的内容，但其 `reference` 属性未被正确验证或限制，可能导致路径遍历攻击。攻击者可以通过构造恶意的 `reference` 参数来包含任意文档的内容，从而获取敏感信息或执行其他操作。
【关键代码模式】`{{include reference="" /}}`，`$request.bindSheet`，`$request.docName`，`$request.spaceName`，`$request.template`
【修复方法说明】修复方法是将 `{{include reference="" /}}` 指令的 `reference` 属性更改为 `{{include reference="" author="target"/}}`，以限制包含文档的范围。这有助于防止路径遍历攻击，确保只有预期的文档内容被包含。


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/SecureHTMLElementSanitizer.java
### 代码内容：
/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
/*
 * Alternatively, at your choice, the contents of this file may be used under the terms of the Mozilla Public License,
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */
package org.xwiki.xml.internal.html;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import org.apache.commons.lang3.StringUtils;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.phase.Initializable;
import org.xwiki.component.phase.InitializationException;
import org.xwiki.stability.Unstable;
import org.xwiki.xml.html.HTMLElementSanitizer;

/**
 * Secure default implementation of {@link HTMLElementSanitizer} based on a definition of allowed elements and
 * attributes.
 *

 * This is heavily inspired by DOMPurify by Cure53 and other contributors | Released under the Apache license 2.0 and
 * Mozilla Public License 2.0 - LICENSE.
 *
 * @version $Id$
 * @since 14.6RC1
 */
@Component
@Named(SecureHTMLElementSanitizer.HINT)
@Singleton
@Unstable
public class SecureHTMLElementSanitizer implements HTMLElementSanitizer, Initializable
{
    /**
     * The hint of this component.
     */
    public static final String HINT = "secure";

    static final Pattern IS_SCRIPT_OR_DATA = Pattern.compile("^(?:\\w+script|data):", Pattern.CASE_INSENSITIVE);

    static final Pattern ATTR_WHITESPACE =
        Pattern.compile("[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]");

    static final Pattern DATA_ATTR = Pattern.compile("^data-[\\-\\w.\\u00B7-\\uFFFF]");

    static final Pattern ARIA_ATTR = Pattern.compile("^aria-[\\-\\w]+$");

    static final Pattern IS_ALLOWED_URI = Pattern.compile("^(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):",
        Pattern.CASE_INSENSITIVE);

    static final Pattern IS_NO_URI = Pattern.compile("^(?:[^a-z]|[a-z+.\\-]+(?:[^a-z+.\\-:]|$))",
        Pattern.CASE_INSENSITIVE);

    @Inject
    private HTMLElementSanitizerConfiguration htmlElementSanitizerConfiguration;

    @Inject
    private HTMLDefinitions htmlDefinitions;

    @Inject
    private SVGDefinitions svgDefinitions;

    @Inject
    private MathMLDefinitions mathMLDefinitions;

    /**
     * Additionally allowed elements.
     */
    private final Set<String> extraAllowedTags;

    /**
     * Additionally allowed attributes.
     */
    private final Set<String> extraAllowedAttributes;

    /**
     * XML attributes that should be allowed.
     */
    private final Set<String> xmlAttributes;

    /**
     * Tags that are safe for data: URIs.
     */
    private final Set<String> dataUriTags;

    /**
     * Attributes safe for values like "javascript:".
     */
    private final Set<String> uriSafeAttributes;

    private final Set<String> forbidTags;

    private final Set<String> forbidAttributes;

    private boolean allowUnknownProtocols;

    private Pattern allowedUriPattern;

    /**
     * Default constructor.
     */
    public SecureHTMLElementSanitizer()
    {
        this.dataUriTags = new HashSet<>(Arrays.asList("audio", "video", "img", "source", "image", "track"));

        this.uriSafeAttributes = new HashSet<>(
            Arrays.asList("alt", "class", "for", "id", "label", "name", "pattern", "placeholder", "role", "summary",
                "title", "value", "style", "xmlns"));

        this.xmlAttributes =
            new HashSet<>(Arrays.asList("xlink:href", "xml:id", "xlink:title", "xml:space", "xmlns:xlink"));

        this.extraAllowedTags = new HashSet<>();

        this.extraAllowedAttributes = new HashSet<>();

        this.forbidTags = new HashSet<>();

        this.forbidAttributes = new HashSet<>();

        this.allowedUriPattern = IS_ALLOWED_URI;
    }

    @Override
    public void initialize() throws InitializationException
    {
        this.extraAllowedTags.addAll(this.htmlElementSanitizerConfiguration.getExtraAllowedTags());
        this.extraAllowedAttributes.addAll(this.htmlElementSanitizerConfiguration.getExtraAllowedAttributes());
        this.uriSafeAttributes.addAll(this.htmlElementSanitizerConfiguration.getExtraUriSafeAttributes());
        this.dataUriTags.addAll(this.htmlElementSanitizerConfiguration.getExtraDataUriTags());
        this.allowUnknownProtocols = this.htmlElementSanitizerConfiguration.isAllowUnknownProtocols();
        this.forbidTags.addAll(this.htmlElementSanitizerConfiguration.getForbidTags());
        this.forbidAttributes.addAll(this.htmlElementSanitizerConfiguration.getForbidAttributes());
        String configuredRegexp = this.htmlElementSanitizerConfiguration.getAllowedUriRegexp();
        if (StringUtils.isNotBlank(configuredRegexp)) {
            this.allowedUriPattern = Pattern.compile(configuredRegexp, Pattern.CASE_INSENSITIVE);
        }
    }

    @Override
    public boolean isElementAllowed(String elementName)
    {
        return !this.forbidTags.contains(elementName)
            && (this.extraAllowedTags.contains(elementName) || isElementSafe(elementName));
    }

    private boolean isElementSafe(String elementName)
    {
        return this.htmlDefinitions.isSafeTag(elementName) || this.svgDefinitions.isSafeTag(elementName)
            || this.mathMLDefinitions.isSafeTag(elementName);
    }

    @Override
    public boolean isAttributeAllowed(String elementName, String attributeName, String attributeValue)
    {
        boolean result = false;

        String lowerElement = elementName.toLowerCase();
        String lowerAttribute = attributeName.toLowerCase();

        if ((DATA_ATTR.matcher(lowerAttribute).find() || ARIA_ATTR.matcher(lowerAttribute).find())
            && !this.forbidAttributes.contains(lowerAttribute))
        {
            result = true;
        } else if (isAttributeAllowed(lowerAttribute) && !this.forbidAttributes.contains(lowerAttribute)) {
            result = isAllowedValue(lowerElement, lowerAttribute, attributeValue);
        }

        return result;
    }

    private boolean isAllowedValue(String lowercaseElementName, String lowercaseAttributeName, String attributeValue)
    {
        // Break into several statements to avoid too long boolean expression.
        boolean result = StringUtils.isBlank(attributeValue);
        if (!result) {
            String valueNoWhitespace = ATTR_WHITESPACE.matcher(attributeValue).replaceAll("");
            result = this.uriSafeAttributes.contains(lowercaseAttributeName);
            result = result || IS_NO_URI.matcher(valueNoWhitespace).find();
            result = result || this.allowedUriPattern.matcher(valueNoWhitespace).find();
            result = result || isAllowedDataValue(lowercaseElementName, lowercaseAttributeName, attributeValue);
            result = result || (this.allowUnknownProtocols && !isScriptOrData(attributeValue));
        }
        return result;
    }

    private boolean isAttributeAllowed(String attributeName)
    {
        boolean result = this.extraAllowedAttributes.contains(attributeName);
        result = result || this.htmlDefinitions.isAllowedAttribute(attributeName);
        result = result || this.svgDefinitions.isAllowedAttribute(attributeName);
        result = result || this.mathMLDefinitions.isAllowedAttribute(attributeName);
        result = result || this.xmlAttributes.contains(attributeName);
        return result;
    }

    private boolean isScriptOrData(String attributeValue)
    {
        return IS_SCRIPT_OR_DATA.matcher(ATTR_WHITESPACE.matcher(attributeValue).replaceAll("")).find();
    }

    private boolean isAllowedDataValue(String elementName, String attributeName, String attributeValue)
    {
        boolean attributeAllowsData = "src".equals(attributeName) || "xlink:href".equals(attributeName)
            || "href".equals(attributeName);
        return attributeAllowsData && !"script".equals(elementName) && attributeValue.startsWith("data:")
            && this.dataUriTags.contains(elementName);
    }
}


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
README.md
### 代码内容：
# [DEPRECATED]
# Please use [Appium Inspector](https://github.com/appium/appium-inspector) and the command line version of [Appium](https://github.com/appium/appium) to inspect elements.

❗❗ This project is no longer maintained since it is not compatible with Appium 2.0+. For Appium 1.x and 2.0+, use the command line Appium server (see the [Appium docs](https://appium.github.io/appium/docs/en/latest/) for installation and setup information), in combination with [Appium Inspector](https://github.com/appium/appium-inspector).

The old documentation for this project remains below.

# Appium Desktop

[![Build Status](https://dev.azure.com/AppiumCI/Appium%20Desktop/_apis/build/status/appium.appium-desktop)](https://dev.azure.com/AppiumCI/Appium%20Desktop/_build/latest?definitionId=1)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/appium-desktop/localized.svg)](https://crowdin.com/project/appium-desktop)

![Action screenshot](docs/images/screen-start-simple.png)

Appium Desktop is an app for Mac, Windows, and Linux which gives you the power of the [Appium](http://appium.io) automation server in a beautiful and flexible UI. It is basically a graphical interface for the Appium Server. You can set options, start/stop the server, see logs, etc... You also don't need to use Node/NPM to install Appium, as the Node runtime comes bundled with Appium Desktop.

**Note:** an inspector UI used to be included with Appium Desktop. It is now its own separate app: [Appium Inspector](https://github.com/appium/appium-inspector).

## Download Appium Desktop

You can always pick up the latest release of the Server GUI at our
[Release](https://github.com/appium/appium-desktop/releases/latest) page on
GitHub.

If you're on Windows or macOS, Appium Desktop will automatically provide you
with updated versions of the app when they are released. If you encounter
a problem updating, simply delete or uninstall the app and re-download the
latest from the link above.

Note that Appium Desktop _is not_ the same thing as Appium. Appium Desktop is
a graphical frontend to Appium with additional tools. Appium Desktop is
released on its own cadence and has its own versioning system. If you are
reporting an issue with Appium Desktop, always be sure to include _both_ the
version of Appium Desktop and the version of the Appium Server which is in use
(see below).

If you're on macOS, you will need to install Appium Desktop apps by copying the app
from the downloaded DMG file to your own file system (the best place is the
"Applications" folder). Running Appium from in side the attached DMG itself is
not supported, and will not work.

### Installing on macOS

If you're using the desktop app on macOS, when you run it you may be greeted with some error about
the app not being able to be opened, or not verified by Apple, or something similar. The easiest
way to get around this is to run `xattr -cr` on the file you downloaded. So let's say you
downloaded `Appium-Server-GUI-mac-<version>.dmg` and copy `Appium Server GUI.app` in
`/Applications` inside the disk image. Then you would run `xattr -cr "/Applications/Appium Server
GUI.app"` before opening it. The same goes for the zip version (or the .app itself).

## Known Issues

* Some Windows 10 Users experience a `PathTooLongException` when installing the EXE. The workaround for this is to update the setting on Windows to [enable long paths](https://superuser.com/questions/1119883/windows-10-enable-ntfs-long-paths-policy-option-missing)

## Usage Instructions

These instructions assume you are already familiar with Appium and Appium-related concepts. If you
are new to Appium, please visit [appium.io](http://appium.io) and read our introductory material.
They also assume that you have downloaded both the Server GUI and the Inspector apps.

This app provides a convenient way to download and run the Appium automation
server, as well as a tool for inspecting elements in Chrome/Safari browser and your Android or iOS application. Its
various capabilities are described in the following sections.

#### Starting a simple server

![Start a basic server](docs/images/screen-start-simple.png)

When you open Appium Desktop, you are greeted with the server start window. The
basic option is to start an Appium server with all its defaults and the ability
to modify the host and port. The start button will also let you know which
version of the Appium server you are running, which can be useful when
reporting issues to the Appium team.

#### Starting a server with advanced options

![Start an advanced server](docs/images/screen-start-advanced.png)

By clicking on the 'Advanced' tab, you have the ability to set all the server
flags that are available in Appium. This is for advanced users and should only
be modified after consulting the Appium documentation.

#### Server presets

![Server presets](docs/images/screen-start-presets.png)

If you use the advanced server options, you have the ability to save
a configuration for later use. Simply save the preset on the 'Advanced' tab,
and you will subsequently be able to recall and start the server with that
configuration from the 'Preset' tab.

### The server console output window

Once you start the server, it will launch on the host and port you specified,
and open a new window displaying the server log output.

![Server console](docs/images/screen-logs.png)

This is fairly straightforward and no real interaction is possible, beyond
using the button to stop the server. You can also copy-and-paste the logs from
this window which is useful in reporting Appium issues.

## Reporting Issues and Requesting Features

Appium Desktop is open source, and we use GitHub for issue tracking. Please
simply report issues at our [issue
tracker](https://github.com/appium/appium-desktop/issues). We will endeavor to
determine whether the issue you are reporting is related to Appium Desktop or
Appium Server. If it's not related to Appium Desktop specifically, we will
close the issue and ask you to open a general Appium issue at [Appium's main
issue tracker](https://github.com/appium/appium/issues). Please, save
yourselves and us valuable time by getting clear on whether the issue you're
experiencing is related to Appium Desktop specifically or instead is a general
Appium issue. You can do this by seeing whether the issue reproduces with the
Appium command line server as well. If it does, direct your report to Appium's
issue tracker.

Have a feature request? Follow the same process and submit an issue to the
appropriate tracker! (Either here in this repo if the request is specifically
for Appium Desktop, or Appium's main tracker if the request is for Appium more
generally.)

## Advanced Topics and Troubleshooting

#### Appium can't detect environment variables on Mac

Appium uses environment variables like `ANDROID_HOME` as well as relying on
various binaries in your `PATH` and so on. When running from the command line
in an environment where you have set these variables appropriately, Appium has
no problem in picking them up. However, Appium Desktop does not run in a shell
or a command-line environment, and so by default it does not have access to
environment variables you have set in your shell startup script or profile. To
work around this, we use the
[shell-env](https://github.com/sindresorhus/shell-env) package to pick up
environment variables defined in your shell. This package only looks in certain
common init scripts, however, like `~/.bashrc`, `~/.bash_profile`, and
`~/.zshrc`. If you set your Appium environment variables in some other way, you
will need to create one of these default init scripts and set your environment
variables there as well, so that Appium Desktop will successfully pick them up.

#### Warnings about being on a read-only file system

This probably means you tried to launch Appium Desktop from the downloaded disk
image (`.dmg` file). This is not a supported mode of running Appium Desktop. To
correctly install Appium Desktop, copy the application from the disk image to
your local filesystem, to somewhere like `/Applications`. Then, run the app
from that new location.

## Developer Instructions

Want to hack on Appium Desktop? Awesome! Head on over to our [Contributing
Doc](CONTRIBUTING.md) for information on how to get a dev environment set up
and submit changes back to the project.

Output:
【漏洞类型】安全漏洞警告
【严重性】Medium
【漏洞摘要】在README.md中，原始代码没有提及任何安全风险。然而，修复后的代码中加入了关于安全漏洞的警告，指出该项目已弃用，并且由于其开放端口可能允许远程代码执行，存在安全风险。尽管没有明确的代码漏洞，这种警告提醒用户注意潜在的风险，属于中等严重性的问题。
【关键代码模式】安全警告、弃用项目、远程代码执行风险。
【修复方法说明】在README.md中加入安全警告，提醒用户不要使用已弃用的Appium Desktop项目，以避免潜在的远程代码执行风险。


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
src/tsd/HttpQuery.java
### 代码内容：
// This file is part of OpenTSDB.
// Copyright (C) 2010-2012  The OpenTSDB Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 2.1 of the License, or (at your
// option) any later version.  This program is distributed in the hope that it
// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser
// General Public License for more details.  You should have received a copy
// of the GNU Lesser General Public License along with this program.  If not,
// see <http://www.gnu.org/licenses/>.
package net.opentsdb.tsd;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

import net.opentsdb.core.Const;
import net.opentsdb.core.TSDB;
import net.opentsdb.graph.Plot;
import net.opentsdb.stats.Histogram;
import net.opentsdb.stats.StatsCollector;
import net.opentsdb.utils.PluginLoader;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.DefaultFileRegion;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.spi.ThrowableProxy;
import ch.qos.logback.classic.spi.ThrowableProxyUtil;

import com.stumbleupon.async.Deferred;

/**
 * Binds together an HTTP request and the channel on which it was received.
 *
 * It makes it easier to provide a few utility methods to respond to the
 * requests.
 */
final class HttpQuery extends AbstractHttpQuery {

  private static final Logger LOG = LoggerFactory.getLogger(HttpQuery.class);

  private static final String HTML_CONTENT_TYPE = "text/html; charset=UTF-8";

  /** The maximum implemented API version, set when the user doesn't */
  private static final int MAX_API_VERSION = 1;

  /**
   * Keep track of the latency of HTTP requests.
   */
  private static final Histogram httplatency =
    new Histogram(16000, (short) 2, 100);

  /** Maps Content-Type to a serializer */
  private static HashMap<String, Constructor<? extends HttpSerializer>>
    serializer_map_content_type = null;

  /** Maps query string names to a serializer */
  private static HashMap<String, Constructor<? extends HttpSerializer>>
    serializer_map_query_string = null;

  /** Caches serializer implementation information for user access */
  private static ArrayList<HashMap<String, Object>> serializer_status = null;

  /** API version parsed from the incoming request */
  private int api_version = 0;

  /** The serializer to use for parsing input and responding */
  private HttpSerializer serializer = null;

  /** Whether or not to show stack traces in the output */
  private final boolean show_stack_trace;

  /**
   * Constructor.
   * @param request The request in this HTTP query.
   * @param chan The channel on which the request was received.
   */
  public HttpQuery(final TSDB tsdb, final HttpRequest request, final Channel chan) {
    super(tsdb, request, chan);
    this.show_stack_trace =
      tsdb.getConfig().getBoolean("tsd.http.show_stack_trace");
    this.serializer = new HttpJsonSerializer(this);
  }

  /**
   * Collects the stats and metrics tracked by this instance.
   * @param collector The collector to use.
   */
  public static void collectStats(final StatsCollector collector) {
    collector.record("http.latency", httplatency, "type=all");
  }

  /**
   * Returns the version for an API request. If the request was for a deprecated
   * API call (such as /q, /suggest, /logs) this value will be 0. If the request
   * was for a new API call, the version will be 1 or higher. If the user does
   * not supply a version, the MAX_API_VERSION value will be used.
   * @since 2.0
   */
  public int apiVersion() {
    return this.api_version;
  }

  /** @return Whether or not to show stack traces in errors @since 2.0 */
  public boolean showStackTrace() {
    return this.show_stack_trace;
  }

  /**
   * Return the {@link Deferred} associated with this query.
   */
  public Deferred<Object> getDeferred() {
    return deferred;
  }

  /** @return The selected seralizer. Will return null if {@link #setSerializer}
   * hasn't been called yet @since 2.0  */
  public HttpSerializer serializer() {
    return this.serializer;
  }

  /**
   * Helper that strips the api and optional version from the URI array since
   * api calls only care about what comes after.
   * E.g. if the URI is "/api/v1/uid/assign" this method will return the
   * {"uid", "assign"}
   * @return An array with 1 or more components, note the first item may be
   * an empty string if given just "/api" or "/api/v1"
   * @throws BadRequestException if the URI is empty or does not start with a
   * slash
   * @throws NullPointerException if the URI is null
   * @throws IllegalArgumentException if the uri does not start with "/api"
   * @since 2.0
   */
  public String[] explodeAPIPath() {
    final String[] split = this.explodePath();
    int index = 1;
    if (split.length < 1 || !split[0].toLowerCase().equals("api")) {
      throw new IllegalArgumentException("The URI does not start with \"/api\"");
    }
    if (split.length < 2) {
      // given "/api"
      final String[] root = { "" };
      return root;
    }
    if (split[1].toLowerCase().startsWith("v") && split[1].length() > 1 &&
        Character.isDigit(split[1].charAt(1))) {
      index = 2;
    }

    if (split.length - index == 0) {
      // given "/api/v#"
      final String[] root = { "" };
      return root;
    }

    final String[] path = new String[split.length - index];
    int path_idx = 0;
    for (int i = index; i < split.length; i++) {
      path[path_idx] = split[i];
      path_idx++;
    }
    return path;
  }

  /**
   * This method splits the query path component and returns a string suitable
   * for routing by {@link RpcHandler}. The resulting route is always lower case
   * and will consist of either an empty string, a deprecated API call or an
   * API route. API routes will set the {@link #apiVersion} to either a user
   * provided value or the MAX_API_VERSION.
   *

   * Some URIs and their routes include:

   *
"/" - "" - the home directory

   *
"/q?start=1h-ago&m=..." - "q" - a deprecated API call

   *
"/api/v4/query" - "api/query" - a versioned API call

   *
"/api/query" - "api/query" - a default versioned API call

   *

   * @return the base route
   * @throws BadRequestException if the version requested is greater than the
   * max or the version # can't be parsed
   * @since 2.0
   */
  @Override
  public String getQueryBaseRoute() {
    final String[] split = explodePath();
    if (split.length < 1) {
      return "";
    }
    if (!split[0].toLowerCase().equals("api")) {
      return split[0].toLowerCase();
    }
    // set the default api_version so the API call is handled by a serializer if
    // an exception is thrown
    this.api_version = MAX_API_VERSION;
    if (split.length < 2) {
      return "api";
    }
    if (split[1].toLowerCase().startsWith("v") && split[1].length() > 1 &&
        Character.isDigit(split[1].charAt(1))) {
      try {
        final int version = Integer.parseInt(split[1].substring(1));
        if (version > MAX_API_VERSION) {
          throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
              "Requested API version is greater than the max implemented",
              "API version [" + version + "] is greater than the max [" +
              MAX_API_VERSION + "]");
        }
        this.api_version = version;
      } catch (NumberFormatException nfe) {
        throw new BadRequestException(HttpResponseStatus.BAD_REQUEST,
            "Invalid API version format supplied",
            "API version [" + split[1].substring(1) +
            "] cannot be parsed to an integer");
      }
    } else {
      return "api/" + split[1].toLowerCase();
    }
    if (split.length < 3){
      return "api";
    }
    return "api/" + split[2].toLowerCase();
  }

  /**
   * Determines the requested HttpMethod via VERB and QS override.
   * If the request is a {@code GET} and the user provides a valid override
   * method in the {@code method=<method>} query string parameter, then
   * the override is returned. If the user supplies an invalid override, an
   * exception is thrown. If the verb was not a GET, then the original value
   * is returned.
   * @return An HttpMethod
   * @throws BadRequestException if the user provided a {@code method} qs
   * without a value or the override contained an invalid value
   * @since 2.0
   */
  public HttpMethod getAPIMethod() {
    if (this.method() != HttpMethod.GET) {
      return this.method();
    } else {
      if (this.hasQueryStringParam("method_override")) {
        final String qs_method = this.getQueryStringParam("method_override");
        if (qs_method == null || qs_method.isEmpty()) {
          throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED,
              "Missing method override value");
        }
        if (qs_method.toLowerCase().equals("get")) {
          // you can't fix dumb
          return HttpMethod.GET;
        } else if (qs_method.toLowerCase().equals("post")){
          return HttpMethod.POST;
        } else if (qs_method.toLowerCase().equals("put")){
          return HttpMethod.PUT;
        } else if (qs_method.toLowerCase().equals("delete")){
          return HttpMethod.DELETE;
        } else {
          throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED,
            "Unknown or unsupported method override value");
        }
      }

      // no override, so just return the method
      return this.method();
    }
  }

  /**
   * Sets the local serializer based on a query string parameter or content type.
   *

   * If the caller supplies a "serializer=" parameter, the proper serializer is
   * loaded if found. If the serializer doesn't exist, an exception will be
   * thrown and the user gets an error
   *

   * If no query string parameter is supplied, the Content-Type header for the
   * request is parsed and if a matching serializer is found, it's used.
   * Otherwise we default to the HttpJsonSerializer.
   * @throws InvocationTargetException if the serializer cannot be instantiated
   * @throws IllegalArgumentException if the serializer cannot be instantiated
   * @throws InstantiationException if the serializer cannot be instantiated
   * @throws IllegalAccessException if a security manager is blocking access
   * @throws BadRequestException if a serializer requested via query string does
   * not exist
   */
  public void setSerializer() throws InvocationTargetException,
    IllegalArgumentException, InstantiationException, IllegalAccessException {
    if (this.hasQueryStringParam("serializer")) {
      final String qs = this.getQueryStringParam("serializer");
      Constructor<? extends HttpSerializer> ctor =
        serializer_map_query_string.get(qs);
      if (ctor == null) {
        this.serializer = new HttpJsonSerializer(this);
        throw new BadRequestException(HttpResponseStatus.BAD_REQUEST,
            "Requested serializer was not found",
            "Could not find a serializer with the name: " + qs);
      }

      this.serializer = ctor.newInstance(this);
      return;
    }

    // attempt to parse the Content-Type string. We only want the first part,
    // not the character set. And if the CT is missing, we'll use the default
    // serializer
    String content_type = request().headers().get("Content-Type");
    if (content_type == null || content_type.isEmpty()) {
      return;
    }
    if (content_type.indexOf(";") > -1) {
      content_type = content_type.substring(0, content_type.indexOf(";"));
    }
    Constructor<? extends HttpSerializer> ctor =
      serializer_map_content_type.get(content_type);
    if (ctor == null) {
      return;
    }

    this.serializer = ctor.newInstance(this);
  }

  /**
   * Sends a 500 error page to the client.
   * Handles responses from deprecated API calls as well as newer, versioned
   * API calls
   * @param cause The unexpected exception that caused this error.
   */
  @Override
  public void internalError(final Exception cause) {
    logError("Internal Server Error on " + request().getUri(), cause);

    if (this.api_version > 0) {
      // always default to the latest version of the error formatter since we
      // need to return something
      switch (this.api_version) {
        case 1:
        default:
          sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR,
              serializer.formatErrorV1(cause));
      }
      return;
    }

    ThrowableProxy tp = new ThrowableProxy(cause);
    tp.calculatePackagingData();
    final String pretty_exc = ThrowableProxyUtil.asString(tp);
    tp = null;
    if (hasQueryStringParam("json")) {
      // 32 = 10 + some extra space as exceptions always have \t's to escape.
      final StringBuilder buf = new StringBuilder(32 + pretty_exc.length());
      buf.append("{\"err\":\"");
      HttpQuery.escapeJson(pretty_exc, buf);
      buf.append("\"}");
      sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, buf);
    } else {
      sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR,
                makePage("Internal Server Error", "Houston, we have a problem",
                         "
"
                         + "
Internal Server Error
"
                         + "Oops, sorry but your request failed due to a"
                         + " server error.

"
                         + "Please try again in 30 seconds.
"
                         + pretty_exc
                         + "

"));
    }
  }

  /**
   * Sends a 400 error page to the client.
   * Handles responses from deprecated API calls
   * @param explain The string describing why the request is bad.
   */
  public void badRequest(final String explain) {
    badRequest(new BadRequestException(explain));
  }

  /**
   * Sends an error message to the client. Handles responses from
   * deprecated API calls.
   * @param exception The exception that was thrown
   */
  @Override
  public void badRequest(final BadRequestException exception) {
    logWarn("Bad Request on " + request().getUri() + ": " + exception.getMessage());
    if (this.api_version > 0) {
      // always default to the latest version of the error formatter since we
      // need to return something
      switch (this.api_version) {
        case 1:
        default:
          sendReply(exception.getStatus(), serializer.formatErrorV1(exception));
      }
      return;
    }
    if (hasQueryStringParam("json")) {
      final StringBuilder buf = new StringBuilder(10 +
          exception.getDetails().length());
      buf.append("{\"err\":\"");
      HttpQuery.escapeJson(exception.getMessage(), buf);
      buf.append("\"}");
      sendReply(HttpResponseStatus.BAD_REQUEST, buf);
    } else {
      sendReply(HttpResponseStatus.BAD_REQUEST,
                makePage("Bad Request", "Looks like it's your fault this time",
                         "
"
                         + "
Bad Request
"
                         + "Sorry but your request was rejected as being"
                         + " invalid.

"
                         + "The reason provided was:
"
                         + exception.getMessage()
                         + "

"));
    }
  }

  /**
   * Sends a 404 error page to the client.
   * Handles responses from deprecated API calls
   */
  @Override
  public void notFound() {
    logWarn("Not Found: " + request().getUri());
    if (this.api_version > 0) {
      // always default to the latest version of the error formatter since we
      // need to return something
      switch (this.api_version) {
        case 1:
        default:
          sendReply(HttpResponseStatus.NOT_FOUND, serializer.formatNotFoundV1());
      }
      return;
    }
    if (hasQueryStringParam("json")) {
      sendReply(HttpResponseStatus.NOT_FOUND,
                new StringBuilder("{\"err\":\"Page Not Found\"}"));
    } else {
      sendReply(HttpResponseStatus.NOT_FOUND, PAGE_NOT_FOUND);
    }
  }

  /** Redirects the client's browser to the given location.  */
  public void redirect(final String location) {
    // set the header AND a meta refresh just in case
    response().headers().set("Location", location);
    sendReply(HttpResponseStatus.OK,
      new StringBuilder(
          "<meta http-equiv=\"refresh\" content=\"0; url="
           + location + "\">")
         .toString().getBytes(this.getCharset())
    );
  }

  /**
   * Escapes a string appropriately to be a valid in JSON.
   * Valid JSON strings are defined in RFC 4627, Section 2.5.
   * @param s The string to escape, which is assumed to be in .
   * @param buf The buffer into which to write the escaped string.
   */
  static void escapeJson(final String s, final StringBuilder buf) {
    final int length = s.length();
    int extra = 0;
    // First count how many extra chars we'll need, if any.
    for (int i = 0; i < length; i++) {
      final char c = s.charAt(i);
      switch (c) {
        case '"':
        case '\\':
        case '\b':
        case '\f':
        case '\n':
        case '\r':
        case '\t':
          extra++;
          continue;
      }
      if (c < 0x001F) {
        extra += 4;
      }
    }
    if (extra == 0) {
      buf.append(s);  // Nothing to escape.
      return;
    }
    buf.ensureCapacity(buf.length() + length + extra);
    for (int i = 0; i < length; i++) {
      final char c = s.charAt(i);
      switch (c) {
        case '"':  buf.append('\\').append('"');  continue;
        case '\\': buf.append('\\').append('\\'); continue;
        case '\b': buf.append('\\').append('b');  continue;
        case '\f': buf.append('\\').append('f');  continue;
        case '\n': buf.append('\\').append('n');  continue;
        case '\r': buf.append('\\').append('r');  continue;
        case '\t': buf.append('\\').append('t');  continue;
      }
      if (c < 0x001F) {
        buf.append('\\').append('u').append('0').append('0')
          .append((char) Const.HEX[(c >>> 4) & 0x0F])
          .append((char) Const.HEX[c & 0x0F]);
      } else {
        buf.append(c);
      }
    }
  }

  /**
   * Sends data in an HTTP "200 OK" reply to the client.
   * @param data Raw byte array to send as-is after the HTTP headers.
   */
  public void sendReply(final byte[] data) {
    sendBuffer(HttpResponseStatus.OK, ChannelBuffers.wrappedBuffer(data));
  }

  /**
   * Sends data to the client with the given HTTP status code.
   * @param status HTTP status code to return
   * @param data Raw byte array to send as-is after the HTTP headers.
   * @since 2.0
   */
  public void sendReply(final HttpResponseStatus status, final byte[] data) {
    sendBuffer(status, ChannelBuffers.wrappedBuffer(data));
  }

  /**
   * Sends an HTTP reply to the client.
   *

   * This is equivalent of
   * {@link #sendReply(HttpResponseStatus, StringBuilder)
   * sendReply}({@link HttpResponseStatus#OK
   * HttpResponseStatus.OK}, buf)
   * @param buf The content of the reply to send.
   */
  public void sendReply(final StringBuilder buf) {
    sendReply(HttpResponseStatus.OK, buf);
  }

  /**
   * Sends an HTTP reply to the client.
   *

   * This is equivalent of
   * {@link #sendReply(HttpResponseStatus, StringBuilder)
   * sendReply}({@link HttpResponseStatus#OK
   * HttpResponseStatus.OK}, buf)
   * @param buf The content of the reply to send.
   */
  public void sendReply(final String buf) {
    sendBuffer(HttpResponseStatus.OK,
               ChannelBuffers.copiedBuffer(buf, CharsetUtil.UTF_8));
  }

  /**
   * Sends an HTTP reply to the client.
   * @param status The status of the request (e.g. 200 OK or 404 Not Found).
   * @param buf The content of the reply to send.
   */
  public void sendReply(final HttpResponseStatus status,
                        final StringBuilder buf) {
    sendBuffer(status, ChannelBuffers.copiedBuffer(buf.toString(),
                                                   CharsetUtil.UTF_8));
  }

  /**
   * Sends the ChannelBuffer with a 200 status
   * @param buf The buffer to send
   * @since 2.0
   */
  public void sendReply(final ChannelBuffer buf) {
    sendBuffer(HttpResponseStatus.OK, buf);
  }

  /**
   * Sends the ChannelBuffer with the given status
   * @param status HttpResponseStatus to reply with
   * @param buf The buffer to send
   * @since 2.0
   */
  public void sendReply(final HttpResponseStatus status,
      final ChannelBuffer buf) {
    sendBuffer(status, buf);
  }

  /**
   * Send a file (with zero-copy) to the client with a 200 OK status.
   * This method doesn't provide any security guarantee.  The caller is
   * responsible for the argument they pass in.
   * @param path The path to the file to send to the client.
   * @param max_age The expiration time of this entity, in seconds.  This is
   * not a timestamp, it's how old the resource is allowed to be in the client
   * cache.  See RFC 2616 section 14.9 for more information.  Use 0 to disable
   * caching.
   */
  public void sendFile(final String path,
                       final int max_age) throws IOException {
    sendFile(HttpResponseStatus.OK, path, max_age);
  }

  /**
   * Send a file (with zero-copy) to the client.
   * This method doesn't provide any security guarantee.  The caller is
   * responsible for the argument they pass in.
   * @param status The status of the request (e.g. 200 OK or 404 Not Found).
   * @param path The path to the file to send to the client.
   * @param max_age The expiration time of this entity, in seconds.  This is
   * not a timestamp, it's how old the resource is allowed to be in the client
   * cache.  See RFC 2616 section 14.9 for more information.  Use 0 to disable
   * caching.
   */
  @SuppressWarnings("resource") // Clears warning about RandomAccessFile not
      // being closed. It is closed in operationComplete().
  public void sendFile(final HttpResponseStatus status,
                       final String path,
                       final int max_age) throws IOException {
    if (max_age < 0) {
      throw new IllegalArgumentException("Negative max_age=" + max_age
                                         + " for path=" + path);
    }
    if (!channel().isConnected()) {
      done();
      return;
    }
    RandomAccessFile file;
    try {
      file = new RandomAccessFile(path, "r");
    } catch (FileNotFoundException e) {
      logWarn("File not found: " + e.getMessage());
      if (getQueryString() != null && !getQueryString().isEmpty()) {
        getQueryString().remove("png");  // Avoid potential recursion.
      }
      this.sendReply(HttpResponseStatus.NOT_FOUND, serializer.formatNotFoundV1());
      return;
    }
    final long length = file.length();
    {
      final String mimetype = guessMimeTypeFromUri(path);
      response().headers().set(HttpHeaders.Names.CONTENT_TYPE,
                         mimetype == null ? "text/plain" : mimetype);
      final long mtime = new File(path).lastModified();
      if (mtime > 0) {
        response().headers().set(HttpHeaders.Names.AGE,
                           (System.currentTimeMillis() - mtime) / 1000);
      } else {
        logWarn("Found a file with mtime=" + mtime + ": " + path);
      }
      response().headers().set(HttpHeaders.Names.CACHE_CONTROL,
                         "max-age=" + max_age);
      HttpHeaders.setContentLength(response(), length);
      channel().write(response());
    }
    final DefaultFileRegion region = new DefaultFileRegion(file.getChannel(),
                                                           0, length);
    final ChannelFuture future = channel().write(region);
    future.addListener(new ChannelFutureListener() {
      public void operationComplete(final ChannelFuture future) {
        region.releaseExternalResources();
        done();
      }
    });
    if (!HttpHeaders.isKeepAlive(request())) {
      future.addListener(ChannelFutureListener.CLOSE);
    }
  }

  /**
   * Method to call after writing the HTTP response to the wire.
   */
  @Override
  public void done() {
    final int processing_time = processingTimeMillis();
    httplatency.add(processing_time);
    logInfo("HTTP " + request().getUri() + " done in " + processing_time + "ms");
    deferred.callback(null);
  }

  /**
   * Sends an HTTP reply to the client.
   * @param status The status of the request (e.g. 200 OK or 404 Not Found).
   * @param buf The content of the reply to send.
   */
  private void sendBuffer(final HttpResponseStatus status,
                          final ChannelBuffer buf) {
    final String contentType = (api_version < 1 ? guessMimeType(buf) :
      serializer.responseContentType());
    sendBuffer(status, buf, contentType);
  }

  /**
   * Returns the result of an attempt to guess the MIME type of the response.
   * @param buf The content of the reply to send.
   */
  private String guessMimeType(final ChannelBuffer buf) {
    final String mimetype = guessMimeTypeFromUri(request().getUri());
    return mimetype == null ? guessMimeTypeFromContents(buf) : mimetype;
  }

  /**
   * Attempts to guess the MIME type by looking at the URI requested.
   * @param uri The URI from which to infer the MIME type.
   */
  private static String guessMimeTypeFromUri(final String uri) {
    final int questionmark = uri.indexOf('?', 1);  // 1 => skip the initial /
    final int end = (questionmark > 0 ? questionmark : uri.length()) - 1;
    if (end < 5) {  // Need at least: "/a.js"
      return null;
    }
    final char a = uri.charAt(end - 3);
    final char b = uri.charAt(end - 2);
    final char c = uri.charAt(end - 1);
    switch (uri.charAt(end)) {
      case 'g':
        return a == '.' && b == 'p' && c == 'n' ? "image/png" : null;
      case 'l':
        return a == 'h' && b == 't' && c == 'm' ? HTML_CONTENT_TYPE : null;
      case 's':
        if (a == '.' && b == 'c' && c == 's') {
          return "text/css";
        } else if (b == '.' && c == 'j') {
          return "text/javascript";
        } else {
          break;
        }
      case 'f':
        return a == '.' && b == 'g' && c == 'i' ? "image/gif" : null;
      case 'o':
        return a == '.' && b == 'i' && c == 'c' ? "image/x-icon" : null;
    }
    return null;
  }

  /**
   * Simple "content sniffing".
   * May not be a great idea, but will do until this class has a better API.
   * @param buf The content of the reply to send.
   * @return The MIME type guessed from {@code buf}.
   */
  private String guessMimeTypeFromContents(final ChannelBuffer buf) {
    if (!buf.readable()) {
      logWarn("Sending an empty result?! buf=" + buf);
      return "text/plain";
    }
    final int firstbyte = buf.getUnsignedByte(buf.readerIndex());
    switch (firstbyte) {
      case '<':  // <html or <!DOCTYPE
        return HTML_CONTENT_TYPE;
      case '{':  // JSON object
      case '[':  // JSON array
        return "application/json";  // RFC 4627 section 6 mandates this.
      case 0x89:  // magic number in PNG files.
        return "image/png";
    }
    return "text/plain";  // Default.
  }

  /**
   * Loads the serializer maps with present, implemented serializers. If no
   * plugins are loaded, only the default implementations will be available.
   * This method also builds the status map that users can access via the API
   * to see what has been implemented.
   *

   * WARNING: The TSDB should have called on of the JAR load or search
   * methods from PluginLoader before calling this method. This will only scan
   * the class path for plugins that implement the HttpSerializer class
   * @param tsdb The TSDB to pass on to plugins
   * @throws NoSuchMethodException if a class could not be instantiated
   * @throws SecurityException if a security manager is present and causes
   * trouble
   * @throws ClassNotFoundException if the base class couldn't be found, for
   * some really odd reason
   * @throws IllegalStateException if a mapping collision occurs
   * @since 2.0
   */
  public static void initializeSerializerMaps(final TSDB tsdb)
    throws SecurityException, NoSuchMethodException, ClassNotFoundException {
    List<HttpSerializer> serializers =
      PluginLoader.loadPlugins(HttpSerializer.class);

    // add the default serializers compiled with OpenTSDB
    if (serializers == null) {
      serializers = new ArrayList<HttpSerializer>(1);
    }
    final HttpSerializer default_serializer = new HttpJsonSerializer();
    serializers.add(default_serializer);

    serializer_map_content_type =
      new HashMap<String, Constructor<? extends HttpSerializer>>();
    serializer_map_query_string =
      new HashMap<String, Constructor<? extends HttpSerializer>>();
    serializer_status = new ArrayList<HashMap<String, Object>>();

    for (HttpSerializer serializer : serializers) {
      final Constructor<? extends HttpSerializer> ctor =
        serializer.getClass().getDeclaredConstructor(HttpQuery.class);

      // check for collisions before adding serializers to the maps
      Constructor<? extends HttpSerializer> map_ctor =
        serializer_map_content_type.get(serializer.requestContentType());
      if (map_ctor != null) {
        final String err = "Serializer content type collision between \"" +
        serializer.getClass().getCanonicalName() + "\" and \"" +
        map_ctor.getClass().getCanonicalName() + "\"";
        LOG.error(err);
        throw new IllegalStateException(err);
      }
      serializer_map_content_type.put(serializer.requestContentType(), ctor);

      map_ctor = serializer_map_query_string.get(serializer.shortName());
      if (map_ctor != null) {
        final String err = "Serializer name collision between \"" +
        serializer.getClass().getCanonicalName() + "\" and \"" +
        map_ctor.getClass().getCanonicalName() + "\"";
        LOG.error(err);
        throw new IllegalStateException(err);
      }
      serializer_map_query_string.put(serializer.shortName(), ctor);

      // initialize the plugins
      serializer.initialize(tsdb);

      // write the status for any serializers OTHER than the default
      if (serializer.shortName().equals("json")) {
        continue;
      }
      HashMap<String, Object> status = new HashMap<String, Object>();
      status.put("version", serializer.version());
      status.put("class", serializer.getClass().getCanonicalName());
      status.put("serializer", serializer.shortName());
      status.put("request_content_type", serializer.requestContentType());
      status.put("response_content_type", serializer.responseContentType());

      HashSet<String> parsers = new HashSet<String>();
      HashSet<String> formats = new HashSet<String>();
      Method[] methods = serializer.getClass().getDeclaredMethods();
      for (Method m : methods) {
        if (Modifier.isPublic(m.getModifiers())) {
          if (m.getName().startsWith("parse")) {
            parsers.add(m.getName().substring(5));
          } else if (m.getName().startsWith("format")) {
            formats.add(m.getName().substring(6));
          }
        }
      }
      status.put("parsers", parsers);
      status.put("formatters", formats);
      serializer_status.add(status);
    }

    // add the base class to the status map so users can see everything that
    // is implemented
    HashMap<String, Object> status = new HashMap<String, Object>();
    // todo - set the OpenTSDB version
    //status.put("version", BuildData.version);
    final Class<?> base_serializer =
      Class.forName("net.opentsdb.tsd.HttpSerializer");
    status.put("class", default_serializer.getClass().getCanonicalName());
    status.put("serializer", default_serializer.shortName());
    status.put("request_content_type", default_serializer.requestContentType());
    status.put("response_content_type", default_serializer.responseContentType());

    ArrayList<String> parsers = new ArrayList<String>();
    ArrayList<String> formats = new ArrayList<String>();
    Method[] methods = base_serializer.getDeclaredMethods();
    for (Method m : methods) {
      if (Modifier.isPublic(m.getModifiers())) {
        if (m.getName().startsWith("parse")) {
          parsers.add(m.getName().substring(5));
        }
        if (m.getName().startsWith("format")) {
          formats.add(m.getName().substring(6));
        }
      }
    }
    status.put("parsers", parsers);
    status.put("formatters", formats);
    serializer_status.add(status);
  }

  /**
   * Returns the serializer status map.
   * Note: Do not modify this map, it is for read only purposes only
   * @return the serializer status list and maps
   * @since 2.0
   */
  public static ArrayList<HashMap<String, Object>> getSerializerStatus() {
    return serializer_status;
  }

  /**
   * Easy way to generate a small, simple HTML page.
   *

   * Equivalent to {@code makePage(null, title, subtitle, body)}.
   * @param title What should be in the {@code title} tag of the page.
   * @param subtitle Small sentence to use next to the TSD logo.
   * @param body The body of the page (excluding the {@code body} tag).
   * @return A full HTML page.
   */
  public static StringBuilder makePage(final String title,
                                       final String subtitle,
                                       final String body) {
    return makePage(null, title, subtitle, body);
  }

  /**
   * Easy way to generate a small, simple HTML page.
   * @param htmlheader Text to insert in the {@code head} tag.
   * Ignored if {@code null}.
   * @param title What should be in the {@code title} tag of the page.
   * @param subtitle Small sentence to use next to the TSD logo.
   * @param body The body of the page (excluding the {@code body} tag).
   * @return A full HTML page.
   */
  public static StringBuilder makePage(final String htmlheader,
                                       final String title,
                                       final String subtitle,
                                       final String body) {
    final StringBuilder buf = new StringBuilder(
      BOILERPLATE_LENGTH + (htmlheader == null ? 0 : htmlheader.length())
      + title.length() + subtitle.length() + body.length());
    buf.append(PAGE_HEADER_START)
      .append(title)
      .append(PAGE_HEADER_MID);
    if (htmlheader != null) {
      buf.append(htmlheader);
    }
    buf.append(PAGE_HEADER_END_BODY_START)
      .append(subtitle)
      .append(PAGE_BODY_MID)
      .append(body)
      .append(PAGE_FOOTER);
    return buf;
  }

  @Override
  protected Logger logger() {
    return LOG;
  }

  // -------------------------------------------- //
  // Boilerplate (shamelessly stolen from Google) //
  // -------------------------------------------- //

  private static final String PAGE_HEADER_START =
    "<!DOCTYPE html>"
    + ""
    + "<meta http-equiv=content-type content=\"text/html;charset=utf-8\">"
    + "<title>";

  private static final String PAGE_HEADER_MID =
    "</title>\n"
    + "";

  private static final String PAGE_HEADER_END_BODY_START =
    "\n"
    + ""
    + "
"
    + "
"
    + ""
    + "
"
    + "
";

  private static final String PAGE_BODY_MID =
    "
"
    + "

";

  private static final String PAGE_FOOTER =
    "
"
    + "

"
    + "
";

  private static final int BOILERPLATE_LENGTH =
    PAGE_HEADER_START.length()
    + PAGE_HEADER_MID.length()
    + PAGE_HEADER_END_BODY_START.length()
    + PAGE_BODY_MID.length()
    + PAGE_FOOTER.length();

  /** Precomputed 404 page. */
  private static final StringBuilder PAGE_NOT_FOUND =
    makePage("Page Not Found", "Error 404",
             "
"
             + "
Page Not Found
"
             + "The requested URL was not found on this server."
             + "
");

}

Output:
【漏洞类型】跨站脚本攻击（XSS）
【严重性】高
【漏洞摘要】原始代码在处理错误时，直接将用户提供的异常信息输出到HTML页面中，而没有进行适当的HTML转义。这使得攻击者可以通过构造特殊的请求来注入恶意脚本，从而在用户浏览器中执行恶意代码。具体表现为，在`internalError`和`badRequest`方法中，异常信息和错误消息直接被插入到HTML字符串中，未经过滤或转义。
【关键代码模式】`sendReply`方法、`makePage`方法、`HtmlEscapers.htmlEscaper().escape()`、`exception.getMessage()`、`pretty_exc`
【修复方法说明】修复方法通过使用`HtmlEscapers.htmlEscaper().escape()`对用户提供的异常信息和错误消息进行了HTML转义，以防止恶意脚本的注入和执行。