# 一.XSS(payload)

# 1.案例一

# 1.1 前端部分

  • 登陆页
  • 信息页

实现 XSS 攻击的恶意脚本被称为 XSS payload

  • 窃取用户的 Cookie document.cookie
  • 识别用户浏览器 navigator.userAgent
  • 伪造请求 GET POST 请求
  • XSS 钓鱼 通过 XSS 向网页上注入钓鱼链接,让用户访问假冒的网站

# 1.2 后端部分

const express = require("express")
const app = express()
const path = require("path")
const bodyParser = require("body-parser")
const cookieParser = require("cookie-parser")

app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())
app.use(express.static(path.resolve(__dirname, "public")))
app.use(cookieParser())

app.get("/cookie", function(req, res) {
  res.send(req.query.cookie)
})

app.listen(3001)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let comment = JSON.stringify({ content: "<script>alert(100)</script>" })
let options = {
  host: "localhost",
  port: 3002,
  path: "/api/comments",
  method: "POST",
  headers: {
    Cookie: "sessionId=session-1536460279485397.5640618347227",
    "Content-Type": "application/json",
  },
}
let http = require("http")
let req = http.request(options, function(res) {
  res.on("data", function() {
    console.log(res.toString(0))
  })
})
req.write(comment)
req.end()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const express = require("express")
const app = express()
const path = require("path")
const bodyParser = require("body-parser")
const cookieParser = require("cookie-parser")

app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())
app.use(express.static(path.resolve(__dirname, "public")))
app.use(cookieParser())

let defaultComment = {
  time: new Date().toLocaleString(),
  avatar: "http://www.gravatar.com/avatar/836875012qq.com.png",
}
let comments = [{ username: "张三", content: "今天下雨", ...defaultComment }]

app.get("/api/comments", function(req, res) {
  res.json({ code: 0, comments })
})

//后台过滤
function htmlEscape(str) {
  return String(str)
    .replace(/>/g, "&gt;")
    .replace(/</g, "&lt;")
    .replace(/&/g, "&amp;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;")
}

app.post("/api/comments", function(req, res) {
  let session = sessions[req.cookies.sessionId]
  if (session && session.user) {
    let comment = req.body
    comments.push({
      ...defaultComment,
      username: session.user.username,
      content: htmlEscape(comment.content),
    })
    res.json({ code: 0 })
  } else {
    res.json({ code: 1, error: "用户未登陆,不能发表评论" })
  }
})

let users = [
  { username: "a", password: "a" },
  { username: "b", password: "b" },
]

function getAdmin(user) {
  for (let i = 0; i < users.length; i++) {
    if (
      users[i].username == user.username &&
      users[i].password == user.password
    ) {
      return users[i]
    }
  }
}

let sessions = {}

app.post("/api/login", function(req, res) {
  let body = req.body
  let user = getAdmin(body)
  if (user) {
    let sessionId = "session-" + Date.now() + Math.random() * 1000
    res.cookie("sessionId", sessionId, { httpOnly: true })
    sessions[sessionId] = { user }
    res.json({ code: 0, user })
  } else {
    res.json({ code: 1, error: "用户密码错误" })
  }
})

app.listen(3002)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

# 2.httpOnly

给 cookie 设置 httpOnly 属性 脚本无法读取该 Cookie,自己也不能用,非根本解决 XSS

# 2.1 前端部分

login.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>登录</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.css">
</head>

<body>
    <div class="container">
        <div class="row">
            <div class="col-md-12">
                <form onsubmit="login(event)">
                    <div class="form-group">
                        <label for="username">用户名</label>
                        <input id="username" class="form-control" placeholder="用户名">
                    </div>
                    <div class="form-group">
                        <label for="password">密码</label>
                        <input id="password" class="form-control" placeholder="密码">
                    </div>
                    <div class="form-group">
                        <input type="submit" class="btn btn-primary" value="登录">
                    </div>
                </form>
            </div>
        </div>
    </div>
    </div>
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
    <script>
        function login() {
            let username = $('#username').val();
            let password = $('#password').val();
            $.post('/api/login', { username, password }).then(data => {
                if (data.code == 0) {
                    location.href = `/user.html?username=${username}`;
                }
                $('#username').val('');
                $('#password').val('');
            });
        }
    </script>
</body>

</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

user.html

<script>
  document.write(document.cookie)
</script>
1
2
3

# 2.2 后端部分

server.js

let users = [{
    username: 'a',
    password: '123456',
    avatar: 'http://cn.gravatar.com/avatar/01459f970ce17cd9e1e783160ecc951a'
  },
  {
    username: 'b',
    password: '123456',
    avatar: 'http://cn.gravatar.com/avatar/01459f970ce17cd9e1e783160ecc951a'
  }];
let userSessions = {};
app.post('/api/login', function (req, res) {
  let body = req.body;
  let user;
  for (let i = 0; i < users.length; i++) {
    if (body.username == users[i].username && body.password == users[i].password) {
      user = users[i];
      break;
    }
  }
  if (user) {
    const sessionId = 'user*' + Math.random() \* 1000;
    res.cookie('username', user.username);
    res.cookie('sessionId', sessionId, { httpOnly: true });
    userSessions[sessionId] = {};
    res.json({ code: 0, user });
  } else {
    res.json({ code: 1, data: '没有该用户' });
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 3.转义

# 3.1 URL 编码

  • 一般来说,URL 只能使用英文字母(a-zA-Z)、数字(0-9)、-*.~4 个特殊字符以及所有(;,/?:@&=+\$#)保留字符。
  • 如果使用了一些其他文字和特殊字符,则需要通过编码的方式来进行表示
var url1 = "http://www.珠峰培训.com" //包含汉字 encodeURI(url1));//http://www.%E7%8F%A0%E5%B3%B0.com
var url2 = "http://www.a.com?名称=珠峰" //键为汉字
var url3 = "http://a.com?name=?&" //值的内容为特殊符号
1
2
3
  • encodeURI encodeURI 是用来编码 URI 的,最常见的就是编码一个 URL。encodeURI 会将需要编码的字符转换为 UTF-8 的格式。对于保留字符(;,/?:@&=+\$#),以及非转义字符(字母数字以及 -\_.!~\*'())不会进行转义。
  • encodeURI 不转义&、?和= encodeURI(url3);//http://a.com?name=?&
  • encodeURIComponent 是用来编码 URI 参数的,它会跳过非转义字符(字母数字以及-\_.!~\*'())。但会转义 URL 的 保留字符(;,/?:@&=+\$#,encodeURIComponent(url3));// http%3A%2F%2Fa.com%3Fname%3D%3F%26
  • 所有完整编码一个 URL 字符串需要 encodeURI 和 encodeURIComponent 联合使用 console.log(encodeURI('http://a.com?name=') + encodeURIComponent('?&')); http://a.com?name=%3F%26

# 3.2 HTML 编码

在 HTML 中,某些字符是预留的,比如不能使用小于号(<)和大于号(>),这是因为浏览器会误认为它们是标签。如果希望正确地显示预留字符,我们必须在 HTML 源代码中使用字符实体(character entities) HTML 编码分为:

  • HTML 十六进制编码 &#xH;
  • HTML 十进制编码 &#D;
  • HTML 实体编码 < 等

在 HTML 进制编码中其中的数字则是对应字符的 unicode 字符编码。 比如单引号的 unicode 字符编码是 27,则单引号可以被编码为'

function htmlEncode(str) {
  return String(str)
    .replace(/&/g, "&amp;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
}
1
2
3
4
5
6
7
8

完整实体

# 3.3 Javascript 转义

avaScript 中有些字符有特殊用途,如果字符串中想使用这些字符原来的含义,需要使用反斜杠对这些特殊符号进行转义。我们称之为 Javascript 编码

  • 三个八进制数字,如果不够个数,前面补 0,例如 “e” 编码为“\145”
  • 两个十六进制数字,如果不够个数,前面补 0,例如 “e” 编码为“\x65”
  • 四个十六进制数字,如果不够个数,前面补 0,例如 “e” 编码为“\u0065”
  • 对于一些控制字符,使用特殊的 C 类型的转义风格(例如\n 和\r)
var str = "zfpx"";
var str = "zfpx\"";
1
2

# 3.4 输入检查

  • 永远不要相信用户的输入
  • 用户格式判断 白名单
  • 过滤危险字符 去除
  • 事件属性中 加入房间

# 3.5 URL 解析环境

使用之前要做 urlencode()

url 中 link

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>

  <body>
    <div id="intag"></div>
    <div id="tagAttr"></div>
    <div id="inEvent"></div>
    <div id="inLink"></div>
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
    <script>
      function htmlEncode(str) {
        return String(str)
          .replace(/&/g, "&amp;")
          .replace(/"/g, "&quot;")
          .replace(/'/g, "&#39;")
          .replace(/</g, "&lt;")
          .replace(/>/g, "&gt;")
      }

      let data = {
        desc: "<script>alert(1);<\/script>",
        clsName: '"><script>alert(2);<\/script>',
        url: '"><script>alert(3);<\/script>',
        id: '"><script>alert(4);<\/script>',
      }
      $("#intag").html(htmlEncode(data.desc))
      $("#tagAttr").html(
        `<a class = "${htmlEncode(data.clsName)}">标签属性中</a>`
      )

      $("#inEvent").html(
        `<a href="#" onclick = "go('${data.url}')">事件参数</a>`
      )
      $("#inLink").html(
        `<a href="http://localhost:3000/articles/${encodeURI(
          data.id
        )}">link</a>`
      )
      function go(url) {
        console.log(url)
      }

      //使用“\”对特殊字符进行转义,除数字字母之外,小于127使用16进制“\xHH”的方式进行编码,大于用unicode(非常严格模式)。
      var JavaScriptEncode = function(str) {
        var hex = new Array(
          "0",
          "1",
          "2",
          "3",
          "4",
          "5",
          "6",
          "7",
          "8",
          "9",
          "a",
          "b",
          "c",
          "d",
          "e",
          "f"
        )
        function changeTo16Hex(charCode) {
          return "\\x" + charCode.charCodeAt(0).toString(16)
        }

        function encodeCharx(original) {
          var found = true
          var thecharchar = original.charAt(0)
          var thechar = original.charCodeAt(0)
          switch (thecharchar) {
            case "\n":
              return "\\n"
              break //newline
            case "\r":
              return "\\r"
              break //Carriage return
            case "'":
              return "\\'"
              break
            case '"':
              return '\\"'
              break
            case "\&":
              return "\\&"
              break
            case "\\":
              return "\\\\"
              break
            case "\t":
              return "\\t"
              break
            case "\b":
              return "\\b"
              break
            case "\f":
              return "\\f"
              break
            case "/":
              return "\\x2F"
              break
            case "<":
              return "\\x3C"
              break
            case ">":
              return "\\x3E"
              break
            default:
              found = false
              break
          }
          if (!found) {
            if (thechar > 47 && thechar < 58) {
              //数字
              return original
            }

            if (thechar > 64 && thechar < 91) {
              //大写字母
              return original
            }

            if (thechar > 96 && thechar < 123) {
              //小写字母
              return original
            }

            if (thechar > 127) {
              //大于127用unicode
              var c = thechar
              var a4 = c % 16
              c = Math.floor(c / 16)
              var a3 = c % 16
              c = Math.floor(c / 16)
              var a2 = c % 16
              c = Math.floor(c / 16)
              var a1 = c % 16
              return "\\u" + hex[a1] + hex[a2] + hex[a3] + hex[a4] + ""
            } else {
              return changeTo16Hex(original)
            }
          }
        }

        var preescape = str
        var escaped = ""
        var i = 0
        for (i = 0; i < preescape.length; i++) {
          escaped = escaped + encodeCharx(preescape.charAt(i))
        }
        return escaped
      }
    </script>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161