第21天——25天php安全开发

php安全开发

1
2
3
4
5
6
留言板的功能(或者可以说是评论区的功能实现流程):
1. 浏览器输入昵称以及内容进行提交,然后发送给web
2. 再由web,将数据传输给后端的数据库
3. 数据库记录/写入传输过来的值,将其保存到数据库中
4. 然后在将保存的结果(保存成功/保存失败)返回给web
5. web在将这个结果回显到客户机上

创建一个新的数据库

Snipaste_2025-05-25_07-04-34

在刚刚创建的数据库中,新建一个表:

Snipaste_2025-05-25_07-07-27

创建一些列名,然后再给表名取一个名字。

Snipaste_2025-05-25_07-08-19

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
数据库的结构:
1. 数据库名—>数据库表名—>数据库列名
2. 数据库数据:格式类型,长度,键等

数据库连接的一些常见函数:
mysqli_connect() 打开一个到 MySQL 的新的连接。
mysqli_select_db() 更改连接的默认数据库。
mysqli_query() 执行某个针对数据库的查 询。
mysqli_fetch_row() 从结果集中取得一行,并作为枚举数组返回。
mysqli_close() 关闭先前打开的数据库连接。



MYSQL增删改查:
查:select * from 表名 where 列名 条件
增:insert into 表名 列名 1`, ` 列名 2`) value(' 列 1 值 1', ' 列 2 值 2');
删:delete from 表名 where 列名 = 条件
改:update 表名 set 列名 = 数据 where 列名 = 条件

配置php的解析器:

Snipaste_2025-05-25_07-21-12

输出一个hello world看看是否可以正常使用php解析:

Snipaste_2025-05-25_07-23-18

Snipaste_2025-05-25_07-22-43

php和mysql的一些必要知识点

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql的增删改查:
查:select * from 表名 where 列名='条件';
增:insert into 表名(`列名1`,`列名2`) value('列1值1','列2值2');
删:delete from 表名 where 列名='条件';
改:update 表名 set 列名 = 数据 where 列名='条件';


php中关于mysql操作的一些函数
mysqli_connect() #打开一个mysql的新的连接
mysqli_select_db() #更改连接的默认数据库。
mysqli_query() #执行某个针对数据库的查询
mysqli_fetch_row() #从结果集中取得一行,并作为枚举数组返回
mysqli_close() #关闭先前打开的数据库连接

php使用mysqli_result类处理结果的几种方法

fetch_all() 抓取所有的结果行并且以关联数据,数值索引数组,或者两者皆有的方式返回结果集。
fetch_array() 以一个关联数组,数值索引数组,或者两者皆有的方式抓取一行结果。
fetch_object() 以对象返回结果集的当前行。
fetch_row() 以枚举数组方式返回一行结果
fetch_assoc() 以一个关联数组方式抓取一行结果。
fetch_field_direct() 以对象返回结果集中单字段的元数据。
fetch_field() 以对象返回结果集中的列信息。
fetch_fields() 以对象数组返回代表结果集中的列信息。

php的一些全局变量

1. $GLOBALS

  • 用途:引用全局作用域中的所有变量。

  • 示例

    1
    2
    3
    4
    $a = 1;
    function test() {
    echo $GLOBALS['a']; // 输出 1
    }
  • 注意:直接操作全局变量可能导致代码耦合,需谨慎使用。


2. $_SERVER

  • 用途:存储服务器和执行环境的信息(如请求头、路径、脚本位置等)。
  • 常用键值
    • $_SERVER['PHP_SELF']:当前脚本文件名(如 /index.php)。
    • $_SERVER['REQUEST_METHOD']:请求方法(如 GETPOST)。
    • $_SERVER['HTTP_USER_AGENT']:客户端浏览器信息。
    • $_SERVER['REMOTE_ADDR']:客户端 IP 地址。
  • 注意:不同服务器环境可能返回不同的值。

3. $_REQUEST

  • 用途:默认包含 $_GET$_POST$_COOKIE 的数据。

  • 示例

    1
    2
    // URL: index.php?name=Alice
    echo $_REQUEST['name']; // 输出 Alice(来自 $_GET)
  • 注意:存在安全风险(如参数覆盖),建议优先使用 $_GET$_POST


4. $_POST

  • 用途:接收通过 HTTP POST 方法提交的表单数据。

  • 示例

    1
    2
    3
    4
    <form method="post">
    <input type="text" name="username">
    <input type="submit">
    </form>
    1
    echo $_POST['username']; // 获取表单提交的用户名

5. $_GET

  • 用途:获取通过 URL 参数(查询字符串)传递的数据。

  • 示例

    1
    2
    // URL: index.php?id=123
    echo $_GET['id']; // 输出 123
  • 注意:数据暴露在 URL 中,不适合敏感信息。


6. $_FILES

  • 用途:处理通过 HTTP POST 上传的文件。

  • 结构

    1
    2
    3
    4
    $_FILES['file']['name'];    // 客户端原始文件名
    $_FILES['file']['tmp_name'];// 服务器上的临时文件路径
    $_FILES['file']['size']; // 文件大小(字节)
    $_FILES['file']['error']; // 错误代码(0 表示成功)
  • 示例:需结合表单的 enctype="multipart/form-data" 使用。


7. $_ENV

  • 用途:存储系统环境变量(如 PATH、数据库配置等)。

  • 示例

    1
    echo $_ENV['PATH']; // 输出系统 PATH 环境变量
  • 注意:默认可能未启用,需在 php.ini 中配置 variables_order 包含 E


  • 用途:读取客户端发送的 HTTP Cookie。

  • 示例

    1
    2
    setcookie('user', 'Alice', time()+3600);
    echo $_COOKIE['user']; // 输出 Alice
  • 注意:Cookie 数据可能被篡改,需验证来源。


9. $_SESSION

  • 用途:存储会话数据,用于跨页面保持用户状态。

  • 示例

    1
    2
    3
    session_start();
    $_SESSION['user_id'] = 1001; // 存储用户 ID
    echo $_SESSION['user_id']; // 输出 1001
  • 注意:必须先调用 session_start() 才能使用

由于22—25天就是开发一个使用了cookie,session,token,文件上传,文件编辑,文件查看,文件下载。拥有管理页面的。一个登录页。所以这里直接就包含所有,一次写完吧。

首先使用deepseek生成一个好看的前端页面,这里我叫login.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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Yatming的登录页</title>
<style>
:root {
--primary-color: rgba(255, 255, 255, 0.95);
--accent-color: #4a90e2;
--border-radius: 15px;
}

* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
}

body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
/* 修改后的背景设置 */
background: url('wallhaven-7jggko.jpg') no-repeat center center fixed;
background-size: cover;
}

.container {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(12px);
border-radius: var(--border-radius);
padding: 2.5rem;
width: 420px;
position: relative;
border: 1px solid rgba(255, 255, 255, 0.2);
}

.tabs {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
}

.tab {
flex: 1;
padding: 1.2rem;
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 8px;
font-size: 1.1rem;
color: var(--primary-color);
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.tab.active {
background: var(--accent-color);
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(74, 144, 226, 0.3);
}

.form-container {
position: relative;
min-height: 400px;
}

.form {
position: absolute;
width: 100%;
opacity: 0;
transform: translateY(20px);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
visibility: hidden;
}

.form.active {
opacity: 1;
transform: translateY(0);
visibility: visible;
}

.input-group {
margin-bottom: 1.8rem;
position: relative;
}

input {
width: 100%;
padding: 1rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
font-size: 1rem;
color: var(--primary-color);
transition: all 0.3s ease;
}

input::placeholder {
color: rgba(255, 255, 255, 0.7);
}

input:focus {
background: rgba(255, 255, 255, 0.2);
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.2);
}

button[type="submit"] {
width: 100%;
padding: 1.2rem;
background: var(--accent-color);
color: white;
border: none;
border-radius: 8px;
font-size: 1.1rem;
cursor: pointer;
transition: all 0.3s ease;
letter-spacing: 1px;
}

button[type="submit"]:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(74, 144, 226, 0.4);
}

@media (max-width: 480px) {
.container {
width: 90%;
padding: 1.8rem;
}

.tab {
padding: 1rem;
font-size: 1rem;
}
}
</style>
</head>
<body>
<div class="container">
<div class="tabs">
<button class="tab active" onclick="switchForm('login')">登录</button>
<button class="tab" onclick="switchForm('register')">注册</button>
</div>

<div class="form-container">
<!-- 登录表单 -->
<form id="loginForm" class="form active" action="./login.php" method="POST">
<div class="input-group">
<input type="username" name="username" placeholder="用户名" required>
</div>
<div class="input-group">
<input type="password" name="password" placeholder="密码" required>
</div>
<input type="submit" value="登录">
</form>

<!-- 注册表单 -->
<form id="registerForm" class="form" action="./register.php" method="POST">
<div class="input-group">
<input type="username" placeholder="用户名" required>
</div>
<div class="input-group">
<input type="email" placeholder="电子邮箱" required>
</div>
<div class="input-group">
<input type="password" placeholder="密码" required>
</div>
<div class="input-group">
<input type="password" placeholder="确认密码" required>
</div>
<input type="submit" name="register" value="注册">
</form>
</div>
</div>

<script>
function switchForm(formType) {
// 切换选项卡样式
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
event.target.classList.add('active');

// 切换表单显示
document.querySelectorAll('.form').forEach(form => {
form.classList.remove('active');
});
document.getElementById(formType + 'Form').classList.add('active');
}
</script>
</body>
</html>

这里要登录,所以就会涉及数据库,关于php连接mysql有三种方式:

php连接mysql的三种方式

MySQLi - 面向对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
$servername = "localhost";
$username = "username";
$password = "password";

// 创建连接
$conn = new mysqli($servername, $username, $password);

// 检测连接
if ($conn->connect_error) {
die("连接失败: " . $conn->connect_error);
}
echo "连接成功";
?>



//可以看到这里是使用的是这种箭头的方式

MySQLi - 面向过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$servername = "localhost";
$username = "username";
$password = "password";

// 创建连接
$conn = mysqli_connect($servername, $username, $password);

// 检测连接
if (!$conn) {
die("Connection failed: " . mysqli_connect_error());
}
echo "连接成功";
?>

PDO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$servername = "localhost";
$username = "username";
$password = "password";

try {
$conn = new PDO("mysql:host=$servername;", $username, $password);
echo "连接成功";
}
catch(PDOException $e)
{
echo $e->getMessage();
}
?>

PDO为什么可以防止注入(扩展)

预处理(Prepared Statements)是数据库操作中的一种安全机制,用于将SQL代码用户输入的数据分离,从而避免SQL注入攻击,同时提高执行效率。以下是其核心原理和流程:


1. 预处理的核心思想

  • 代码与数据分离
    SQL语句的结构(如表名、字段、条件逻辑)提前定义并固定,用户输入的数据仅作为参数传入,无法修改SQL原有的逻辑结构。
  • 类比
    类似于“填空题”——SQL语句是固定的模板,用户输入的数据只是填入模板中的“空白处”,无法改变题目本身。

2. 预处理的工作流程

步骤1:定义SQL模板(预编译)

开发者编写一个包含占位符的SQL语句模板,例如:

1
2
3
SELECT * FROM users WHERE username = ? AND password = ?;
-- 或者使用命名占位符:
SELECT * FROM users WHERE username = :username AND password = :password;

此时,数据库会解析并编译这个SQL模板,确定其语法和结构(例如检查表名、字段是否存在)。

步骤2:绑定参数

将用户输入的数据(如表单提交的$_POST['username'])绑定到占位符。例如:

1
2
3
4
// 使用PDO绑定参数
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':username', $username);
$stmt->bindParam(':password', $password);

数据库明确知道这些参数是数据而非代码,会对其中的特殊字符(如'";)进行安全处理(例如转义或类型校验)。

步骤3:执行查询

执行时,数据库将预编译的SQL结构和绑定的参数合并,确保参数不会破坏原有逻辑:

1
$stmt->execute();

3. 预处理如何防止SQL注入?

  • 结构固定
    SQL模板在预编译后,逻辑已确定(例如查询的字段、表名、条件顺序)。后续传入的参数无法改变SQL结构,即使包含恶意代码(如' OR 1=1 --),也只会被视为普通字符串。
  • 自动转义
    数据库会根据参数类型(如字符串、整数)自动处理特殊字符。例如,字符串参数中的单引号'会被转义为\',使其失去破坏SQL结构的能力。
  • 绕过手动转义的缺陷
    传统拼接SQL时,依赖开发者手动调用addslashes()等函数,容易遗漏或处理不当。预处理由数据库底层自动处理,更安全可靠。

4. 预处理的额外优势

  1. 性能提升
    • 同一SQL模板多次执行时(如批量插入),数据库只需编译一次,后续直接复用,减少解析开销。
    • 例如:循环插入100条数据,预处理效率远高于拼接100条独立SQL语句。
  2. 类型安全
    • 绑定参数时可指定数据类型(如PDO::PARAM_INT),避免因类型错误导致的意外行为。

5. 示例对比:预处理 vs 普通查询

不安全写法(直接拼接SQL)

1
2
3
4
$username = $_POST['username'];
$password = $_POST['password'];
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
// 若用户输入 username = ' OR 1=1 --,会导致SQL注入!

安全写法(预处理)

1
2
3
4
5
6
7
$sql = "SELECT * FROM users WHERE username = :username AND password = :password";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':username' => $_POST['username'],
':password' => $_POST['password']
]);
// 即使用户输入恶意字符,也会被正确处理为普通字符串。

6. 注意事项

  • 禁用模拟预处理
    PDO默认可能使用“模拟预处理”(在PHP端转义而非数据库原生支持),需关闭以确保安全:

    1
    $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
  • 字符集设置
    连接数据库时指定正确的字符集(如utf8mb4),避免编码漏洞:

    1
    new PDO("mysql:host=localhost;dbname=test;charset=utf8mb4", "user", "pass");

总结

预处理通过分离代码与数据,从根本上消除了SQL注入的可能性,是数据库操作中最关键的安全实践。正确使用预处理,配合参数绑定,能有效保护应用免受恶意输入攻击。

php连接数据库,进行账号密码的判断

为了减少代码重复,一般连接数据库的操作都会封装在一个文件,其他文件想要调用,直接使用include包含即可:

config.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$servername = "localhost";
$username = "root";
$password = "root";
$database = 'myblog';

// 创建连接
$conn = new mysqli($servername, $username, $password,$database);

// 检测连接
if ($conn->connect_error) {
die("连接失败: " . $conn->connect_error);
}else{
echo "连接成功";
}

?>

Snipaste_2025-05-25_18-07-53

login.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
header('Content-type:text/html; charset=utf-8');
include './db/config.php';



$username = $_POST['username'];
$password = $_POST['password'];

$sql = "select * from users where username='$username' and password='$password'";
$result=$conn->query($sql);

$row=$result->fetch_row();
if ($result->num_rows > 0) {//mysqli_num_rows() 函数返回结果集中行的数量,如果账号密码正确就会返回行数,如果账号密码不正确就没有返回值,这里同样可以使用empty这个函数来判断这个返回的值是否为空
//登录成功
echo "登录成功";
} else {
//登录失败
echo "用户名或密码错误";
}

?>

这样就可以简单判断数据库的账号密码是否正确,这里需要手动添加这个数据库中的值

Snipaste_2025-05-25_19-33-04

Snipaste_2025-05-25_19-37-56

Snipaste_2025-05-25_19-38-57

如果想要更改上面这种未授权,那么第一种就是加上else,或者在if判断里面,判断错误之后直接使用die函数进行强制退出。

1
2
3
4
5
小知识点:
一段语句一般最外名使用的是双引号进行包裹,然后里面的变量都是使用单引号进行包裹

下面这个是一个js的语句,用来跳转到某个页面:
window.location.href='login.html'

cookie和session的使用

cookie和session的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cookie:保存在客户端
session:保存在服务端


通俗解释:
Cookie:像超市的储物柜小票(你保存小票,凭它取物)。
例子:记住用户名,下次自动填充。
Session:像银行的保险柜(银行保管物品,给你一把钥匙)。
例子:用户登录后,服务端记录登录状态,浏览器只保存钥匙(Session ID)。


应用场景:
用Cookie:不敏感数据 + 需长期保存(如“记住我”功能)。
用Session:敏感数据或临时交互(如购物车、支付流程)。


安全风险: 不管是cookie还是session,当他们的值可以被预判的时候,那么不管你是是保存在服务端还是客户端都是没有用的,只要能被预测下一个值,或者被劫持,那么这两个都有安全风险。

token:
具有唯一性,就是说,每次登录,然后你再次登录的时候token的值就会发生变化,这个时候并不是说过你不退出登录值就不会更改,意思是每次触发登录这个操作的时候,token就会发生改变。

Snipaste_2025-05-25_20-33-23

上面这种就是数据包中带有cookie,这里的cookie等于username

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
setcookie("username",$username,time()+3600);

设置一个超长时间的cookie:
setcookie("username",$username,time()+(365*86400));
setcookie("username",$username,time()+(365*86400));
如果像这里这样进行设置,那么这里在admin.php的判断页面,就要判断两个值,不能像下面那样只对username进行判断。

第一个字段的意思:是cookie参数的名字
第二个字段的意思:是第一个字段的值
第三个字段的意思:是过期时间,这里用时间戳进行表示。



cookie的格式

setcookie(name,value,expire,path,domain,secure)
name是cookie的名称
value是cookie的值
expire是失效时间
path是cookie的生效路径
domain是cookie的作用域名范围
secure用于指定是否开启https连接来传输cookie

Snipaste_2025-05-25_20-45-20

这里你可以尝试将cookie这个值设置60秒失效,然后观察一下过期之后cookie的字段。

cookie的写法,在后台页面直接判断cookie中有没有我们想要的值(这个值可以伪造)。如果有这个值,那么就直接访问后台页面,如果没有就跳转到登录页面

login.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
header('Content-type:text/html; charset=utf-8');
include './db/config.php';

$username = $_POST['username'];
$password = $_POST['password'];

$sql = "select * from users where username='$username' and password='$password'";
$result=$conn->query($sql);
$row=$result->fetch_row();
var_dump($row);
if ($result->num_rows > 0) {
//登录成功,多种写法
//echo "<script>alert('用户:$username 登录成功');window.location.href='./admin.html'</script>";
setcookie("username",$username,time()+360);
echo '<script>alert("用户:'.$username. '登录成功");window.location.href="./admin.php"</script>';
}else{
//登录失败,多种写法
echo "<script>alert('用户名或密码错误');</script>";
//echo "<script>alert('用户名或密码错误');window.location.href='./login.html'</script>";
}

?>

admin.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>后台管理</title>
</head>
<body>
<h1>这是一个后台页面</h1>
</body>
</html>

<?php

if(empty($_COOKIE['username'])){
echo '<script>alert("没有cookie这个值");window.location.href="./login.html"</script>';

}else{
echo "欢迎来到yatming的后台";
echo '<iframe src="//player.bilibili.com/player.html?isOutside=true&aid=1555837685&bvid=BV141421r7Hq&cid=1590430720&p=1" scrolling="no" width="100%" height="500px" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>';
}

?>

session

login.php

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
<?php
session_start();
header('Content-type:text/html; charset=utf-8');
include './db/config.php';

$username = $_POST['username'];
$password = $_POST['password'];

$sql = "select * from users where username='$username' and password='$password'";
$result=$conn->query($sql);
$row=$result->fetch_row();
var_dump($row);
if ($result->num_rows > 0) {
//登录成功,多种写法
//echo "<script>alert('用户:$username 登录成功');window.location.href='./admin.html'</script>";
//setcookie("username",$username,time()+360);
$_SESSION['username'] = $username; //这里进行session的设置
$_SESSION['password'] = $password; //这里进行session的设置
echo '<script>alert("用户:'.$username. '登录成功");window.location.href="./admin.php"</script>';
}else{
//登录失败,多种写法
//echo "<script>alert('用户名或密码错误');</script>";
echo "<script>alert('用户名或密码错误');window.location.href='./login.html'</script>";
}

?>

admin.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>后台管理</title>
</head>
<body>
<h1>这是一个后台页面</h1>
</body>
</html>

<?php
session_start();

if(empty($_SESSION['username'])){
echo '<script>alert("没有session");window.location.href="./login.html"</script>';

}else{
echo "欢迎来到yatming的后台";
echo '<iframe src="//player.bilibili.com/player.html?isOutside=true&aid=1555837685&bvid=BV141421r7Hq&cid=1590430720&p=1" scrolling="no" width="100%" height="500px" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>';
}

?>

这里你会发现session的设置方法和cookie差不多。只不过session是保存在服务端的。

Snipaste_2025-05-25_22-09-12

这里我用的phpstudy,这个是我的保存目录。

Snipaste_2025-05-25_22-11-09

注册的功能实现

register.php

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
<?php
include './db/config.php';

$username= $_POST['username'];
$password = $_POST['password'];
$emali = $_POST['emali'];
$password2 = $_POST['password2'];
$stmt = $conn->query("SELECT * FROM users WHERE username = '$username' OR mail = '$emali';");
if(!($password == $password2)){
echo '<script>alert("两次密码输入不一致");window.location.href="./login.html"</script>';
}elseif($stmt->num_rows > 0){
// 检查用户名/邮箱是否已存在
echo "邮箱或者用户名已经存在";
}else{
$sql = "INSERT INTO users(username,mail,password) values('$username','$emali','$password');";
$data=$conn->query($sql);
echo '<script>alert("注册成功")</script>';
//$sql2 = "select * from users where username='$username' and password='$password'";
//$result=$conn->query($sql2);
//if ($result->num_rows > 0) {
//$_SESSION['username'] = $username;
//$_SESSION['password'] = $password;
//echo '<script>alert("用户:'.$username. '登录成功");window.location.href="./admin.php"</script>';
//}
$_SESSION['username'] = $username;
$_SESSION['password'] = $password;
echo '<script>alert("用户:'.$username. '登录成功");window.location.href="./admin.php"</script>';

}


?>

admin.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>后台管理</title>
</head>
<body>
<h1>这是一个后台页面</h1>
</body>
</html>

<?php
include './db/config.php';

if(empty($_SESSION['username'])){
echo '<script>alert("没有session");window.location.href="./login.html"</script>';

}else{
echo "欢迎来到yatming的后台";
echo '<iframe src="//player.bilibili.com/player.html?isOutside=true&aid=1555837685&bvid=BV141421r7Hq&cid=1590430720&p=1" scrolling="no" width="100%" height="500px" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>';
}

?>

修改已有账户的密码

1
下面这种写法有一个漏洞就是,你登录之后,可以修改任意用户的账号密码

update 表名 set 列名 = 数据 where 列名 = 条件

edit.php

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
<html>
<h1>修改页面</h1>

<form action="" method="POST">
<div>
<input type="text" name="username" placeholder="用户名" required>
</div>
<div>
<input type="password" name="password" placeholder="密码" required>
</div>
<input type="submit" value="修改密码">
</form>
<td><a href="login.html">回到登录页面</a></td>
</html>

<?php
include './db/config.php';
include 'xhsession.php';



//update 表名 set 列名 = 数据 where 列名 = 条件
$username=$_POST['username'];
$password=$_POST['password'];
if(empty($_SESSION['username']) && empty($_SESSION['password'])){
echo '<script>alert("没有session");window.location.href="./login.html"</script>';
}
else{
if(isset($_POST['username']) && isset($_POST['password'])) {
$sql = "update users set password = '$password' where username='$username'; ";
$result=$conn->query($sql);
session_destroy(); //销毁session
echo '<script>alert("密码更新成功,请重新登录");</script>';

}

}

//$sql = "update users set password = '$password' where username='$username'; ";
//$result=$conn->query($sql);
//if($result->num_rows > 0){
// echo '<script>alert("密码更新成功,请重新登录");window.location.href="./login.html"</script>';
// }else{
// echo "密码更新失败";
// }



?>

xhsession.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
function secureSessionDestroy() {
$_SESSION = array();
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
session_destroy();
session_regenerate_id(true);
}
?>

admin.php

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>后台管理</title>
</head>
<body>
<h1>欢迎来到yatming的后台</h1>
</body>
</html>

<?php
include './db/config.php';

if(empty($_SESSION['username']) && empty($_SESSION['password'])){
echo '<script>alert("没有session");window.location.href="./login.html"</script>';

}else{

echo '<iframe src="//player.bilibili.com/player.html?isOutside=true&aid=1555837685&bvid=BV141421r7Hq&cid=1590430720&p=1" scrolling="no" width="100%" height="500px" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>';

//update 表名 set 列名 = 数据 where 列名 = 条件
echo "<td><a href='./edit.php'>修改密码</a></td>";

}

?>

第二种方式实现注册,登录, 修改密码

解析include,empty函数

index.php

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
<?php
//$type=!empty($_GET['type']) ? $_GET['type'] : 'login';
include_once ("./config/db.php");

$type = $_GET['type'];

if(empty($_GET['type'])){
$type='login';
}


switch($type){
case 'login':
include_once('./template/login.html');
break;

case 'login_api':
$username = $_POST['username'];
$password = $_POST['password'];
$sql = "select * from user where username='$username' and password = '$password';";
//echo $sql;
$result=$db->query($sql);

if($result->num_rows > 0){
echo "<script>alert('$username 登录成功,欢迎你 ');window.location.href='./index.php?type=admin'</script>";
$_SESSION['username'] = $username;
$_SESSION['password'] = $password;
}else{
echo '<script>alert("登录失败,账号或者密码错误");window.location.href="./index.php?type=login"</script>';
}

break;

case 'reregister':
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = trim($_POST['password'] ?? '');
$password2 = trim($_POST['password2'] ?? '');

// 验证数据完整性
if (empty($username) || empty($password)) {
die('<script>alert("用户名和密码不能为空");history.back()</script>');
}

// 验证密码一致性
if ($password !== $password2) {
die('<script>alert("两次密码输入不一致");history.back()</script>');
}

// 检查用户名是否已存在
$check = $db->prepare("SELECT id FROM user WHERE username = ?");
$check->bind_param("s", $username);
$check->execute();
if ($check->get_result()->num_rows > 0) {
die('<script>alert("用户名已存在");history.back()</script>');
}

// 哈希加密密码
$hashed_pw = password_hash($password, PASSWORD_DEFAULT);

// 使用预处理语句防止SQL注入
$stmt = $db->prepare("INSERT INTO user (username, password) VALUES (?, ?)");
$stmt->bind_param("ss", $username, $hashed_pw);

if ($stmt->execute() && $stmt->affected_rows > 0) {
// 注册成功但不要自动登录!应跳转到登录页
$_SESSION['username'] = $username;
$_SESSION['password'] = $password;
echo '<script>alert("注册成功,请登录");location.href="./index.php?type=admin"</script>';
exit;
} else {
die('<script>alert("注册失败,请重试");history.back()</script>');
}
} else {
// 非POST请求时显示注册表单
include_once('./template/reergister.html');
}
break;

case 'admin':
if(empty($_SESSION['username'])){
echo '<script>alert("你尚未登录到网站之中!");window.location.href="./index.php?type=login"</script>';
}else{
echo '<script>alert("登录到后台中");window.location.href="admin.php"</script>';
}
echo 'hello yatming';
break;

default:
echo 'this is default';
break;

}

?>

admin.php

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>后台管理</title>
</head>
<body>
<h1>欢迎来到yatming的后台</h1>

<br/>
<br/>
<br/>
<br/>
<br/>
<h5><a href="login.html">退出登录</a></h5>
</body>
</html>

<?php
include './config/db.php';

if(empty($_SESSION['username']) ){
echo '<script>alert("没有session");window.location.href="./login.html"</script>';

}else{

echo '<iframe src="//player.bilibili.com/player.html?isOutside=true&aid=1555837685&bvid=BV141421r7Hq&cid=1590430720&p=1" scrolling="no" width="100%" height="500px" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>';
//session_destroy();
//update 表名 set 列名 = 数据 where 列名 = 条件
//echo "<td><a href='./edit.php'>修改密码</a></td>";
}

?>

最终版本——PDO

1
2
3
4
5
预编译(PDO)语句的几个步骤
1、准备(Prepare): 创建一个预处理语句对象,SQL语句中的参数使用占位符(如?或命名占位符如“ :name ”)表示
2、绑定参数(Bind):将变量绑定到占位符(这一步有时在execute中直接传递参数数组代替)
3、执行(Execute):执行预处理语句,将绑定的值插入到SQL语句中并执行
4、获取结果(Fetch):根据需要获取结果

目录结构:

Snipaste_2025-06-01_23-07-57

数据库配置文件:db.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
session_start();
$dbhost = '127.0.0.1';
$username = 'root';
$password = 'root';
$dbname = 'code';

try {
$pdo = new PDO("mysql:host=$dbhost;dbname=$dbname;charset=utf8", $username, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
} catch (PDOException $e) {
die('数据库连接失败: ' . $e->getMessage());
}
?>

函数文件:js.php

1
2
3
4
5
6
7
<?php
function alert_localtion ($msg,$url){
echo '<script>alert("'.$msg.'");window.location.href="'.$url.'"</script>';
die;
}

?>

修改密码前端文件:edit_pass.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<form action="./index.php?type=edit_pass_api" method="post">
<p>用户名:<input type="text" name="username" /></p>
<p>旧密码:<input type="password" name="old_password" /></p>
<p>新密码:<input type="password" name="new_password" /></p>
<input type="submit" value="修改密码" />
</form>

</body>
</html>

登录前端页面:login.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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - 简约苹果风</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}

body {
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8f0 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}

.container {
width: 100%;
max-width: 400px;
}

.apple-logo {
display: block;
margin: 0 auto 30px;
width: 80px;
height: 80px;
background: linear-gradient(135deg, #0071e3, #8e44ad);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}

.apple-logo i {
color: white;
font-size: 40px;
}

.form-container {
background: white;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
padding: 40px 30px;
}

.form-title {
font-size: 1.8rem;
font-weight: 600;
margin-bottom: 30px;
color: #333;
text-align: center;
}

.input-group {
margin-bottom: 25px;
}

.input-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #555;
font-size: 0.95rem;
}

.input-group input {
width: 100%;
padding: 15px 18px;
border: 1px solid #e0e0e0;
border-radius: 12px;
font-size: 1rem;
transition: all 0.3s;
background: #f9f9f9;
}

.input-group input:focus {
outline: none;
border-color: #0071e3;
box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.1);
background: white;
}

.input-group .error {
color: #e74c3c;
font-size: 0.85rem;
margin-top: 6px;
display: none;
}

.input-group.has-error input {
border-color: #e74c3c;
}

.input-group.has-error .error {
display: block;
}

.btn {
width: 100%;
padding: 15px;
border: none;
border-radius: 12px;
background: #0071e3;
color: white;
font-size: 1.1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
margin-top: 15px;
box-shadow: 0 4px 10px rgba(0, 113, 227, 0.2);
}

.btn:hover {
background: #005bb8;
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(0, 113, 227, 0.3);
}

.switch-text {
text-align: center;
margin-top: 25px;
color: #666;
font-size: 0.95rem;
}

.switch-text a {
color: #0071e3;
text-decoration: none;
font-weight: 500;
}

.switch-text a:hover {
text-decoration: underline;
}

.security-info {
background: #f8f9fa;
border-radius: 12px;
padding: 15px;
margin-top: 20px;
border: 1px solid #eee;
font-size: 0.85rem;
color: #666;
text-align: center;
}

.security-info i {
color: #0071e3;
margin-right: 8px;
}

.footer {
text-align: center;
margin-top: 30px;
color: #777;
font-size: 0.9rem;
}
</style>
</head>
<body>
<div class="container">
<div class="apple-logo">
<i class="fas fa-lock"></i>
</div>

<div class="form-container">
<h2 class="form-title">登录您的账户</h2>

<form id="loginForm" action="./index.php?type=login_api" method="post">
<div class="input-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" required>
<div class="error" id="usernameError">输入包含不安全字符</div>
</div>

<div class="input-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" required>
<div class="error" id="passwordError">输入包含不安全字符</div>
</div>

<button type="submit" class="btn">登录</button>
</form>

<div class="security-info">
<p><i class="fas fa-shield-alt"></i> 安全系统检测SQL注入关键词</p>
</div>

<div class="switch-text">
还没有账户? <a href="register.html">立即注册</a>
</div>
</div>

<div class="footer">
<p>&copy; 2023 简约账户系统 | 苹果风格设计</p>
</div>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
// SQL注入关键词列表
const sqlKeywords = [
'select', 'insert', 'update', 'delete', 'drop',
'alter', 'create', 'truncate', 'union', 'join',
'or', 'and', ';', '--', '/*', '*/', 'xp_', 'exec'
];

// 表单提交处理
const loginForm = document.getElementById('loginForm');
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');

loginForm.addEventListener('submit', function(e) {
// 重置错误状态
usernameInput.parentElement.classList.remove('has-error');
passwordInput.parentElement.classList.remove('has-error');

let hasSqlInjection = false;

// 检查用户名
if (containsSqlKeywords(usernameInput.value)) {
usernameInput.parentElement.classList.add('has-error');
hasSqlInjection = true;
}

// 检查密码
if (containsSqlKeywords(passwordInput.value)) {
passwordInput.parentElement.classList.add('has-error');
hasSqlInjection = true;
}

// 如果发现SQL注入关键词,阻止表单提交
if (hasSqlInjection) {
e.preventDefault();
}
});

// 检查是否包含SQL关键词
function containsSqlKeywords(value) {
const lowerValue = value.toLowerCase();
for (const keyword of sqlKeywords) {
if (lowerValue.includes(keyword)) {
return true;
}
}
return false;
}
});
</script>
<!--测试账户:test 密码: 123.comyatming -->
<!-- bG92ZS55YXRtaW5nLnRvcA== -->
</body>
</html>

处理所有功能的php文件:index.php

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
<?php
include_once './config/db.php';
include_once './function/js.php';

$type = !empty($_GET['type']) ? $_GET['type'] : 'login';

$No_Check_Controller_array = array('login','login_api','register','register_api');

if(!(in_array($type,$No_Check_Controller_array))){
if(empty($_SESSION['username'])){
alert_localtion("尚未登录,正在跳转到登录页面..........","./index.php?type=login");
}
}

switch($type){
case 'login':
include_once('./login.html');
session_destroy();
break;

case 'login_api':
$username = $_POST['username'];
$password = $_POST['password'];

if(!empty($username) && !empty($password)){
try {
$sql = "SELECT * FROM blog WHERE username = :username AND password = :password";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':username', $username, PDO::PARAM_STR);
$stmt->bindParam(':password', $password, PDO::PARAM_STR);
$stmt->execute();

if($stmt->rowCount() > 0){
$row = $stmt->fetch(PDO::FETCH_ASSOC);

if($row['status'] == '0'){
alert_localtion("账号封禁","./index.php?type=login");
}

$_SESSION['username'] = $username;
$_SESSION['password'] = $password;
alert_localtion("Hello $username 登录成功","./index.php?type=index");
} else {
alert_localtion("登录失败,账号或者密码错误!","./index.php?type=login");
}
} catch (PDOException $e) {
alert_localtion("数据库错误: ".$e->getMessage(),"./index.php?type=login");
}
}
break;

case 'register':
include_once('./register.html');
break;

case 'register_api':
$username = $_POST['username'];
$password = $_POST['password'];
$password2 = $_POST['password2'];

if(empty($password) || empty($username) || empty($password2)){
echo "请填写完整";
} else {
if($password === $password2){
try {
// 检查用户是否存在
$sql = "SELECT * FROM blog WHERE username = :username";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':username', $username, PDO::PARAM_STR);
$stmt->execute();

if($stmt->rowCount() > 0){
alert_localtion("该用户已经存在","./index.php?type=login");
} else {
// 插入新用户
$sql = "INSERT INTO blog (username, password, status) VALUES (:username, :password, '1')";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':username', $username, PDO::PARAM_STR);
$stmt->bindParam(':password', $password, PDO::PARAM_STR);
$stmt->execute();

$_SESSION['username'] = $username;
$_SESSION['password'] = $password;
alert_localtion("注册成功,请登录","./index.php?type=login");
}
} catch (PDOException $e) {
alert_localtion("数据库错误: ".$e->getMessage(),"./index.php?type=register");
}
} else {
echo "密码不一致";
}
}
break;

case 'edit_pass':
include_once('./edit_pass.html');
break;

case 'edit_pass_api':
$username = $_POST['username'];
$old_password = $_POST['old_password'];
$new_password = $_POST['new_password'];

if(empty($old_password) || empty($username) || empty($new_password)){
echo "请填写完整";
} else {
try {
// 验证旧密码
$sql = "SELECT * FROM blog WHERE username = :username AND password = :old_password";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':username', $username, PDO::PARAM_STR);
$stmt->bindParam(':old_password', $old_password, PDO::PARAM_STR);
$stmt->execute();

if($stmt->rowCount() > 0){
// 更新密码
$sql = "UPDATE blog SET password = :new_password WHERE username = :username";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':new_password', $new_password, PDO::PARAM_STR);
$stmt->bindParam(':username', $username, PDO::PARAM_STR);
$stmt->execute();

session_destroy();
alert_localtion("密码修改成功!","./index.php?type=login");
} else {
alert_localtion("账号不存在,或者密码不对!","./index.php?type=edit_pass");
}
} catch (PDOException $e) {
alert_localtion("数据库错误: ".$e->getMessage(),"./index.php?type=edit_pass");
}
}
break;

case 'index':
echo 'hello '.$_SESSION['username'].' 欢迎登录到个人中心';
echo '<br/>';
echo '<br/>';
echo '<a href="./index.php?type=edit_pass">修改密码</a><br/><br/>';
echo '<a href="http://10.20.0.198/ly/index.php">留言板</a>';
break;

default:
echo '默认页';
break;
}
?>

注册的前端文件:register.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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>注册 - 简约苹果风</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}

body {
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8f0 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}

.container {
width: 100%;
max-width: 400px;
}

.apple-logo {
display: block;
margin: 0 auto 30px;
width: 80px;
height: 80px;
background: linear-gradient(135deg, #0071e3, #8e44ad);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}

.apple-logo i {
color: white;
font-size: 40px;
}

.form-container {
background: white;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
padding: 40px 30px;
}

.form-title {
font-size: 1.8rem;
font-weight: 600;
margin-bottom: 30px;
color: #333;
text-align: center;
}

.input-group {
margin-bottom: 25px;
}

.input-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #555;
font-size: 0.95rem;
}

.input-group input {
width: 100%;
padding: 15px 18px;
border: 1px solid #e0e0e0;
border-radius: 12px;
font-size: 1rem;
transition: all 0.3s;
background: #f9f9f9;
}

.input-group input:focus {
outline: none;
border-color: #0071e3;
box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.1);
background: white;
}

.input-group .error {
color: #e74c3c;
font-size: 0.85rem;
margin-top: 6px;
display: none;
}

.input-group.has-error input {
border-color: #e74c3c;
}

.input-group.has-error .error {
display: block;
}

.btn {
width: 100%;
padding: 15px;
border: none;
border-radius: 12px;
background: #0071e3;
color: white;
font-size: 1.1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
margin-top: 15px;
box-shadow: 0 4px 10px rgba(0, 113, 227, 0.2);
}

.btn:hover {
background: #005bb8;
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(0, 113, 227, 0.3);
}

.switch-text {
text-align: center;
margin-top: 25px;
color: #666;
font-size: 0.95rem;
}

.switch-text a {
color: #0071e3;
text-decoration: none;
font-weight: 500;
}

.switch-text a:hover {
text-decoration: underline;
}

.security-info {
background: #f8f9fa;
border-radius: 12px;
padding: 15px;
margin-top: 20px;
border: 1px solid #eee;
font-size: 0.85rem;
color: #666;
text-align: center;
}

.security-info i {
color: #0071e3;
margin-right: 8px;
}

.footer {
text-align: center;
margin-top: 30px;
color: #777;
font-size: 0.9rem;
}
</style>
</head>
<body>
<div class="container">
<div class="apple-logo">
<i class="fas fa-user-plus"></i>
</div>

<div class="form-container">
<h2 class="form-title">创建新账户</h2>

<form id="registerForm" action="./index.php?type=register_api" method="post">
<div class="input-group">
<label for="regUsername">用户名</label>
<input type="text" id="regUsername" name="username" required>
<div class="error" id="regUsernameError">输入包含不安全字符</div>
</div>

<div class="input-group">
<label for="regPassword">密码</label>
<input type="password" id="regPassword" name="password" required>
<div class="error" id="regPasswordError">输入包含不安全字符</div>
</div>

<div class="input-group">
<label for="regPassword2">确认密码</label>
<input type="password" id="regPassword2" name="password2" required>
<div class="error" id="regPassword2Error">输入包含不安全字符</div>
</div>

<button type="submit" class="btn">注册</button>
</form>

<div class="security-info">
<p><i class="fas fa-shield-alt"></i> 安全系统检测SQL注入关键词</p>
<p><i class="fas fa-exclamation-triangle"></i> 检测关键词: select, insert, update, delete, drop, or, and, ;, -- 等</p>
</div>

<div class="switch-text">
已有账户? <a href="login.html">返回登录</a>
</div>
</div>

<div class="footer">
<p>&copy; 2023 简约账户系统 | 苹果风格设计</p>
</div>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
// SQL注入关键词列表
const sqlKeywords = [
'select', 'insert', 'update', 'delete', 'drop',
'alter', 'create', 'truncate', 'union', 'join',
'or', 'and', ';', '--', '/*', '*/', 'xp_', 'exec'
];

// 表单提交处理
const registerForm = document.getElementById('registerForm');
const usernameInput = document.getElementById('regUsername');
const passwordInput = document.getElementById('regPassword');
const password2Input = document.getElementById('regPassword2');

registerForm.addEventListener('submit', function(e) {
// 重置所有错误状态
document.querySelectorAll('.input-group').forEach(group => {
group.classList.remove('has-error');
});

let hasSqlInjection = false;

// 检查所有输入字段
const inputs = [
{element: usernameInput, errorId: 'regUsernameError'},
{element: passwordInput, errorId: 'regPasswordError'},
{element: password2Input, errorId: 'regPassword2Error'}
];

inputs.forEach(input => {
if (containsSqlKeywords(input.element.value)) {
input.element.parentElement.classList.add('has-error');
hasSqlInjection = true;
}
});

// 如果发现SQL注入关键词,阻止表单提交
if (hasSqlInjection) {
e.preventDefault();
}
});

// 检查是否包含SQL关键词
function containsSqlKeywords(value) {
const lowerValue = value.toLowerCase();
for (const keyword of sqlKeywords) {
if (lowerValue.includes(keyword)) {
return true;
}
}
return false;
}
});
</script>
</body>
</html>

Snipaste_2025-06-02_07-16-49

留言板实现

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
<?php
// 数据库配置
$db_host = 'localhost';
$db_user = 'root';
$db_pass = 'root';
$db_name = 'message_board';

// 连接数据库
$pdo = new PDO("mysql:host=$db_host;dbname=$db_name;charset=utf8mb4", $db_user, $db_pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// 处理留言提交(使用GET方式)
if (isset($_GET['content'])) {
$content = $_GET['content'];
$ua = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown UA';

$stmt = $pdo->prepare("INSERT INTO `messages` (`content`, `ua`) VALUES (:content, :ua)");
$stmt->execute([
':content' => $content,
':ua' => $ua
]);

header('Location: ' . $_SERVER['PHP_SELF']);
exit;
}

// 获取所有留言
$stmt = $pdo->query("SELECT * FROM `messages` ORDER BY `create_time` DESC");
$messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>留言板</title>
<style>
body { max-width: 600px; margin: 0 auto; padding: 20px; }
.message { border: 1px solid #ddd; margin: 10px 0; padding: 10px; }
.ua-info { color: #666; font-size: 0.9em; }
</style>
</head>
<body>
<h1>留言板</h1>


<form method="get">
<textarea name="content" rows="5" cols="50" placeholder="输入留言内容..." required></textarea><br>
<button type="submit">提交</button>
</form>

<h2>最新留言</h2>
<?php foreach ($messages as $msg): ?>
<div class="message">
<?php echo $msg['content']; ?>
<div class="ua-info">UA: <?php echo htmlspecialchars($msg['ua']); ?></div> <!-- UA头进行了转义 -->
</div>
<?php endforeach; ?>
</body>
</html>

Snipaste_2025-06-02_07-15-42

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
个人认为的缺陷:
1、首先是只要可以注册成功就可以登录后台, 应该写一个后台管理员界面和普通用户后台,然后分开进行鉴权。
2、这里虽然进行了预编译,但是只有对登录和注册进行了预编译,没有对后台进行进一步的判断,这里也是由于第一个的原因导致所有注册的用户都可以登录到后台,只要我在注册的时候写一条xss语句注册成用户,然后在登录到后台的时候由于后台会显示登录者的用户名,没有对这个用户名做任何的过滤,所以就会执行xss。
payload:")</script><script>alert(1)//

3、这个留言板也没有对xss有任何的过滤,我是在后端部署了一个安全狗,但是安全狗只对特殊的标签进行过滤,如果我使用如下payload,依旧可以绕过安全狗。
payload:
<p><input onclick="alert(2)">2</p>
<button onclick="alert(1)">1</button>
<a href="https://www.yatming.top" onclick="alert(document.cookie)">跳转</a>
<div onclick="alert(1)">1</div>
<div onmouseover="alert(1)">11231</div>
<iframe onload=alert("xss");></iframe>
<svg onload=alert(1)>
<body onload="alert(1)">

<body onscroll=alert("xss");><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><input autofocus>

<details open ontoggle="alert('xss');">

留言板删除的思路

1
删除的思路,首先你要对每个数据有一个唯一标识符号,比如说id,或者内容,但是如果选择内容的话,一个用户有多个相同的内容该怎么判断(个人理解:如果根据内容进行筛选一个人的数据,那么如果出现一个人有多条相同数据的时候可以只删除一条,或者增加时间字段,但是这样过于麻烦)。还是建议有一个唯一字段,拥有了唯一字段之后,你可以使用`a`标签创建一个链接,这个链接可以将唯一的字段在后端使用get或者post的方式进行接收,当接收到这个值的时候,执行delele语句,条件就是传输过来的唯一字段,然后执行sql语句就行了。当然这里同样要进行鉴权,因为如果不进行鉴权,所有人都可以删除任何人的数据。所以这里在sql判断的时候还要加上username字段,比如:`delete from xiaodi where username =xxx and id = xx`

原生的AJAX

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
小知识:一般的js语句都是在代码的底部引入,css文件都是在html文件的头部引入。这样可以减少网站的加载时间(并不是物理上的减少,而是感官上的)

ajax和from表单提交的区别:

1、提交的方式
form表单通常是通过在html定义的cation,method和submit来进行表单提交,另外也可以通过在js中调用submit函数进行表单提交

2、页面的刷新
form提交,更新数据完成后,需要跳转到一个空白的页面,然后子在对原网页进行处理,哪怕是提交给自己本身的页面,也是需要刷新的,因此局限性很大。
ajax,$http都可以实现页面的局部刷新,整个页面不会进行刷新。

3、由谁来提交
from提交是浏览器完成的,无论是浏览器是否开启js,都可以提交表单
ajax,$http是通过js来提交请求,请求和响应均由js引擎来处理,因此不启用js的浏览器,无法完成该操作。


原生ajax创建的五个步骤

1、使用ajax发送数据的步骤

第一步:创建异步对象

1
var xhr = new XMLHttpRequest();

第二步:设置请求行open(请求方式,请求url):

1
2
3
//get请求如果有参数就在url后面拼接参数
//post如果有参数,就在请求体传递 xhr.open("get","validate.php?username=")
xhr.open("post","validate.php");

第三步:设置请求(GET方式忽略此步骤)头:setRequestHeader()

1
2
3
//1.get不需要设置,因为GET:请求资源(不需要请求体)
//2.post需要设置请求头:content-Type:application/x-www-form-urlencoded
xhr.setRequestHeader('Content-Type', 'application/json');

第四步:定义onload监听器

1
2
3
xhr.onload = function() { /* 处理成功响应 */ };
xhr.onerror = function() { /* 处理网络错误 */ };
xhr.ontimeout = function() { /* 处理超时 */ };

第五步:发送请求体send()

1
xhr.send(body); 

完整流程示意图:

1
2
3
4
5
6
7
8
9
10
11
      创建XHR对象

open()配置

(可选)设置请求头

定义事件监听器 ←─────┐
↓ │
send() │
│ │
└──→ 等待响应 → 触发监听器

错误风险:若先调用send()再设置onload监听器,可能导致事件丢失

1
2
3
4
5
6
7
8
9
10
<script type = "text/javascript">
var xhr = new XMLHttpRequest();
xhr.open("post","vaildate.php");
xhr.onreadystatechange = function(){
if(xhr.status == 200 && xhr.readyState == 4){
console.log(xhr.responseText);
}
}
xhr.send("username=admin"+"&password=123456");
</script>

可以在浏览器中使用F12进行调试:

Snipaste_2025-05-27_09-05-10

这里报错是因为请求不到这个文件,这个文件不存在,然后可以在网络,如上图,这里的载荷可以看到上图按照我们写的代理写的请求体去发送了请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script type = "text/javascript">

function login(){
var xhr = new XMLHttpRequest();
xhr.open("post","vaildate.php");
xhr.onreadystatechange = function(){
if(xhr.status == 200 && xhr.readyState == 4){
console.log(xhr.responseText);
}
}
xhr.send("username=admin"+"&password=123456");
}

</script>

将其封装一个函数,然后在F12中调用他:

Snipaste_2025-05-27_09-09-45

js获取数据的两种方式

1
2
3
4
5
6
7
8
9
getElementById()方法:通过id取得HTML元素。

getElementsByName()方法:通过name取得元素,是一个数组。

还有就是
document.getElementsByName("usernaem")[0].value
//value一般是用在input这种类型提交数据框里面获取里面的值
document.getElementsByName("usernaem")[0].innerHTML
//innerHTML是用来获取html标签里面的值

通过name获取页面中的值:

1
document.getElementsByName("username")[0].value

Snipaste_2025-05-27_09-20-14

通过ID获取页面中的值

1
document.getElementById("usertext").value 

Snipaste_2025-05-27_09-25-23

如果你要通过id获取页面中的值,那么相对应的html中也要有这个值。

1
2
3
小知识点:
js中console.log是用来打印变量的。跟printf类似的作用。
js中使用的是 + 进行字符串连接,类似于之前的 . 连接符

那么可以获取到值之后,如何让用户点击登录的时候自动执行这个函数呢?使用onclick=""点击事件

原生态ajax:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script src="./jquery-3.7.1.js"></script>
<script type = "text/javascript">

function login(){
var username = document.getElementsByName("username")[0].value;
var password = document.getElementsByName("password")[0].value;
var post_data = "username=" + username + "&password=" + password
var xhr = new XMLHttpRequest();
xhr.open("post","index.php?type=login_api");
xhr.send(post_data);
xhr.onreadystatechange = function(){
if(xhr.status == 200 && xhr.readyState == 4){
console.log(xhr.responseText);
}
}

}

</script>

jquery_AJAX

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
<script src="./jquery-3.7.1.js"></script>
<script type = "text/javascript">

// function login(){
// var username = document.getElementsByName("username")[0].value;
// var password = document.getElementsByName("password")[0].value;
// var post_data = "username=" + username + "&password=" + password
// var xhr = new XMLHttpRequest();
// xhr.open("post","index.php?type=login_api");
// xhr.send(post_data);
// xhr.onreadystatechange = function(){
// if(xhr.status == 200 && xhr.readyState == 4){
// console.log(xhr.responseText);
// }
// }

// }
function login() {
var username = document.getElementsByName("username")[0].value;
var password = document.getElementsByName("password")[0].value;
data = {
"username":username,
"password":password
}
$.ajax(
{
url: "./index.php?type=login_api",
type: 'POST',
data : data,
async: true,
success:(function(result){
console.log(result)
})

}
)
}


</script>

Snipaste_2025-05-27_13-42-43

这里就可以从上图看到点击按钮之后触发login函数。

总结:jquery_ajax的方式更加好用。且代码更容易看懂。

1
小知识点:如果你想要form表单停止跳转的话,那么只需要在form表单中加入:action="javascript:;"

Snipaste_2025-05-27_13-45-09

上面这里使用ajax是不能使页面进行跳转的,现在实现这个登录成功就可以跳转的功能(这里只对原理进行研究,不提供源代码了)

json数据格式

1. 可读性强

  • 结构清晰:使用键值对(key: value)和层级缩进,直观展示数据关系,适合人类阅读和调试。

2. 跨语言兼容

  • 广泛支持:几乎所有编程语言(如 Python、Java、C#、PHP 等)都有内置或第三方库解析和生成 JSON。
  • 数据交换通用:不同系统或服务间传递数据时,JSON 作为“中间语言”减少兼容问题。

3. 轻量高效

  • 体积小:相比 XML,JSON 没有冗余标签,节省带宽,加快网络传输速度。
  • 解析速度快:语法简单,序列化(生成)和反序列化(解析)效率高。

Snipaste_2025-05-27_14-04-17

这里是一条跳转语句,就是登录之后跳转的地址,但是这里没有明确写出具体的链接地址(他是由后端传输到前端,而不是直接写到前端的),如果这里明确写出来的话对于攻击者来说就可以直接获取到后台的地址。

MVC形式架构

MVC(Model-View-Controller) 是一种经典的软件设计架构模式,用于将应用程序的逻辑、数据和用户界面分离,提升代码的可维护性、可扩展性和复用性。它将应用程序划分为三个核心组件:

1. Model(模型)

  • 职责: 处理数据和业务逻辑。
  • 包含:
    • 数据结构(如数据库表、对象)。
    • 数据访问操作(如增删改查)。
    • 业务规则和验证逻辑(如用户注册验证、订单计算)。
  • 特点: 不关心用户界面如何显示,也不直接处理用户输入。只负责管理应用程序的核心数据和功能。

2. View(视图)

  • 职责: 展示数据(给用户看)和接收用户输入。
  • 包含: 用户界面元素(UI),如网页(HTML/CSS)、图形界面(GUI)、命令行输出等。
  • 特点:
    • Model获取需要显示的数据(通常通过Controller)。
    • 将用户的操作(点击按钮、输入文本)传递给Controller
    • 通常不包含复杂的业务逻辑,主要负责展示

3. Controller(控制器)

  • 职责: 作为ModelView之间的协调者,处理用户输入并更新模型和视图。
  • 包含:
    • 接收来自View的用户输入(如HTTP请求、点击事件)。
    • 根据输入调用相应的Model进行数据处理或状态变更(如保存数据、计算结果)。
    • 选择并更新合适的View来响应用户(如渲染新的页面、刷新部分界面)。
  • 特点: 是应用程序的“交通警察”,决定用户请求的流向。

小迪篇

这个是目录结构:

Snipaste_2025-06-01_20-14-30

index.php

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- 配置文件 -->
<!-- <scripttype="text/javascript"src="./ueditor/ueditor.config.js"></script> -->
<!-- 编辑器源码文件 -->
<!-- <scripttype="text/javascript"src="./ueditor/ueditor.all.js"></script> -->
<script src="./ueditor/ueditor.config.js">/*引入配置文件*/</script>
<script src="./ueditor/ueditor.all.js">/*引入源码文件*/</script>
</head>
<body>
<script src="/ueditor/ueditor.config.js">/*引入配置文件*/</script>
<script src="/ueditor/ueditor.all.js">/*引入源码文件*/</script>
<form action="" method="post">
<br/>
<input type="text" placeholder="用户名" name="username" />
<br/>
<br/>


<textarea id="content" placeholder="请输入留言..." name="content" rows="6" cols="50" ></textarea>
<br/>
<br/>


<script type="text/javascript">
UE.getEditor("content");

//实例化编辑器传参,id为将要被替换的容器。
</script>
<button type="submit">提交</button>
</form>

<br/><br/><br/>
<a href="./login.html">登录后台</a>
</body>
</html>

<?php
include "./config/db.php";

function getClientIP() {
$ip = '';

if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP']; // 共享互联网IP
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR']; // 代理服务器转发链
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
$ip = $_SERVER['REMOTE_ADDR']; // 直接连接IP
}

// 处理多个IP的情况(取第一个)
if (strpos($ip, ',') !== false) {
$ipList = explode(',', $ip);
$ip = trim($ipList[0]);
}

return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : 'unknown';
}

function add_liuyan($pdo) {

$username = $_POST['username'];
$content = $_POST['content'];
$ip_addr = getClientIP();
$ua_gent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';

//插入留言
if(!empty($content) && !empty($username) ){
$sql = "INSERT INTO xiaodi (username, content, ip_addr,ua_gent) VALUES (:username, :content, :ip_addr,:ua_gent)";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':username', $username, PDO::PARAM_STR);
$stmt->bindParam(':content', $content, PDO::PARAM_STR);
$stmt->bindParam(':ip_addr', $ip_addr, PDO::PARAM_STR);
$stmt->bindParam(':ua_gent', $ua_gent, PDO::PARAM_STR);
$stmt->execute();
}else{
echo "<script>alert('用户或者内容未输入')</script>";
}
}

function show_liuyan($pdo) {
//查询留言
$sql = "SELECT * FROM xiaodi;";
$stmt = $pdo->prepare($sql);
$stmt->execute();


while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
echo "<hr/>";
echo "<br/>";
echo "<br/>";
echo "用户: " . $row['username'] . "<br>";
echo "内容: " . $row['content'] . "<br>";
echo "ip地址: " . $row['ip_addr'] . "<br>";
echo "UA头: " . $row['ua_gent'] . "<br>";
}
}

show_liuyan($pdo);
add_liuyan($pdo);

/*
预编译(PDO)语句的几个步骤
1、准备(Prepare): 创建一个预处理语句对象,SQL语句中的参数使用占位符(如?或命名占位符如“ :name ”)表示
2、绑定参数(Bind):将变量绑定到占位符(这一步有时在execute中直接传递参数数组代替)
3、执行(Execute):执行预处理语句,将绑定的值插入到SQL语句中并执行
4、获取结果(Fetch):根据需要获取结果

*/
?>

admin.php

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
<?php
include "../config/db.php";
include "../login.php";

if(empty($_SESSION['username'])){
echo '你的权限不够!';
die;

}
//删除留言
$del = $_GET['del'];
$sql = "delete from xiaodi where content = '$del';";
$stmt = $pdo->prepare($sql);
$stmt->execute();

//查询留言
$sql = "SELECT * FROM xiaodi;";
$stmt = $pdo->prepare($sql);
$stmt->execute();


while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
echo "<hr/>";
echo "<br/>";
echo "<br/>";
echo "用户: " . $row['username'] . "<br>";
echo "内容: " . $row['content'] . "<br>";
echo "ip地址: " . $row['ip_addr'] . "<br>";
echo "UA头: " . $row['ua_gent'] . "<br>";
echo "<a href='admin.php?del=" . $row['content'] . "'>删除</a>";
}



?>

login.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
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户登录</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f0f2f5;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}

.login-container {
background-color: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
width: 320px;
}

h2 {
color: #1877f2;
text-align: center;
margin-bottom: 1.5rem;
}

.form-group {
margin-bottom: 1rem;
}

input {
width: 100%;
padding: 12px;
border: 1px solid #dddfe2;
border-radius: 6px;
margin-top: 0.5rem;
}

button {
background-color: #1877f2;
color: white;
width: 100%;
padding: 12px;
border: none;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
}

button:hover {
background-color: #166fe5;
}

.links {
text-align: center;
margin-top: 1rem;
}

a {
color: #1877f2;
text-decoration: none;
}
</style>
</head>
<body>
<div class="login-container">
<h2>用户登录</h2>
<form id="loginForm" action="./login.php" method="post">
<div class="form-group">
<input type="text" placeholder="用户名" name="username" />
</div>
<div class="form-group">
<input type="password" placeholder="密码" name="password" />
</div>
<button type="submit">登录</button>
</form>
<div class="links">
<a href="./index.php?type=register">注册新账号</a>
</div>
</div>
</body>
</html>

db.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
session_start();
$dbhost = '127.0.0.1';
$username = 'root';
$password = 'root';
$dbname = 'xiaodi';

try {
$pdo = new PDO("mysql:host=$dbhost;dbname=$dbname;charset=utf8", $username, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
} catch (PDOException $e) {
die('数据库连接失败: ' . $e->getMessage());
}
?>

login.php

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
<?php
include "./config/db.php";
include "./js.php";

$username=$_POST['username'];
$password=$_POST['password'];

if(!empty($username) && !empty($password)){
try {
$sql = "SELECT * FROM xiaodi WHERE username = :username AND password = :password";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':username', $username, PDO::PARAM_STR);
$stmt->bindParam(':password', $password, PDO::PARAM_STR);
$stmt->execute();

if($stmt->rowCount() > 0){

$_SESSION['username'] = $username;
$_SESSION['password'] = $password;
alert_localtion("Hello $username 登录成功","./admin/admin.php");
} else {
alert_localtion("登录失败,账号或者密码错误!","./index.php");
}
} catch (PDOException $e) {
alert_localtion("数据库错误: ".$e->getMessage(),"./index.php");
}
}



?>

个人认为的缺陷

1
2
3
4
1、第一个就是只有admin用户可以删除数据(懒的在一步一步的写单独的注册页面和对每个用户进行判断了)
2、就是下面我提到的,我这里对唯一字段的判断就是内容,当一个用户有多条相同内容的时候,显然这样判断是有问题的。
3、安全问题:没有对用户的输入进行验证,可以实现xss攻击。同时如果实现了单个用户删除自身的留言,但是过滤不严谨,就会导致越权。
4、如果要增加功能的话,无非就是注册,修改密码,以及留言板本身的更新,修改。删除功能我已经实现。

界面:

Snipaste_2025-06-01_20-51-00

上面已经使用session了,如果使用cookie也是同理,下面写入token的验证方式

token验证

token.php

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
<?php
// 生成Token并将其存储在Session中
session_start();


//1.因为是用的session维持会话,token已经绑定到下面的表单了
//2.token,生成之后直接存到session里,主要是方便重置token,
//每次token随表单提交后都需要重置以保持token的唯一性。

$_SESSION['token'] = bin2hex(random_bytes(16));


?>

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>后台登录</title>
<style>
body {
background-color: #f1f1f1;
}
.login {
width: 400px;
margin: 100px auto;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0,0,0,0.3);
padding: 30px;
}
.login h2 {
text-align: center;
font-size: 2em;
margin-bottom: 30px;
}
.login label {
display: block;
margin-bottom: 20px;
font-size: 1.2em;
}
.login input[type="text"], .login input[type="password"] {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 1.2em;
margin-bottom: 20px;
}
.login input[type="submit"] {
background-color: #2ecc71;
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 5px;
font-size: 1.2em;
cursor: pointer;
}
.login input[type="submit"]:hover {
background-color: #27ae60;
}
</style>
</head>
<body>
<div class="login">
<h2>后台登录</h2>
<form action="token_check.php" method="post">
<input type="hidden" name="token" value="<?php echo $_SESSION['token'] ; ?>">
<label for="username">用户名:</label>
<input type="text" name="username" id="username" required>
<label for="password">密码:</label>
<input type="password" name="password" id="password" required>
<input type="submit" value="登录">
</form>
</div>
</body>
</html>

token_check.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
session_start();

$token = $_POST['token'] ?? '';

if ($token !== $_SESSION['token']) {
// token不匹配,禁止访问
header('HTTP/1.1 403 Forbidden');
$_SESSION['token'] = bin2hex(random_bytes(16));
echo 'Access denied';
exit;
}else{
$_SESSION['token'] = bin2hex(random_bytes(16));
if($_POST['username']=='admin' && $_POST['password']=='123456'){
echo '登录成功!';
echo '你是管理员可以访问文件管理页面!';
}else{
echo '登录失败!';
}
}

代码解析:

1
首先token.php就是在php的表单中输出了$_SESSION['token'] 使其name可以获取到这个值,然后就可以在后面的token_check.php文件中对他进行赋值。这样操作的话,if判断中的else条件是无论如何都会成立的。也就是说每次提交数据都会进行进行账号和密码的判断,然后重新获取一次token值,如果你使用暴力破解,那么就会出现if中的第一个条件不满足。从而触发Access denied。除非你的第一次暴力破解的第一个数据包就成功了。

文件上传

文件上传的前端页面:upload.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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件上传页面</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f2f2f2;
padding: 20px;
}
h1 {
text-align: center;
margin-top: 50px;
}
form {
background-color: #fff;
border-radius: 10px;
padding: 20px;
margin-top: 30px;
max-width: 600px;
margin: 0 auto;
}
input[type="file"] {
margin-top: 20px;
margin-bottom: 20px;
}
button {
background-color: #4CAF50;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background-color: #3e8e41;
}
</style>
</head>
<body>
<h1>文件上传</h1>
<form action="./upload.php" method="POST" enctype="multipart/form-data">
<label for="file">选择文件:</label>
<br>
<input type="file" id="file" name="f">
<br>
<button type="submit">上传文件</button>
</form>
</body>
</html>

<!--

enctype="multipart/form-data的作用

文件上传支持
当表单中包含<input type="file">(文件选择控件)时,必须使用此编码类型,否则文件内容无法正确传输。

数据编码方式
它将表单数据分割成多个独立的部分(parts),每个字段/文件作为独立的区块传输,用特殊边界字符串分隔(如------WebKitFormBoundary7MA4YWxkTrZu0gW)。

传输内容类型
允许同时传输二进制数据(如图片、视频、文档等)和普通文本数据(如文本输入框的内容)。

-->

这里文件上传就不得不提$_FILES这个全局变量。他有就几个核心的参数,如下:

参数 描述 示例值
name 上传文件的原始文件名(含扩展名) "my_photo.jpg"
type 文件的 MIME 类型(由浏览器提供,不可靠) "image/jpeg"
tmp_name 文件在服务器上的临时存储路径 "/tmp/php/php7h4j1o"
error 上传状态码(整数,0 表示成功) 0 (UPLOAD_ERR_OK)
size 文件大小(字节) 102400 (100KB)

简单运行输出一下,各种信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

$name=$_FILES['file']['name'];
$type=$_FILES['file']['type'];
$size=$_FILES['file']['size'];
$tmp_name=$_FILES['file']['tmp_name'];
$error=$_FILES['file']['error'];

echo $name."<br>";
echo $type."<br>";
echo $size."<br>";
echo $tmp_name."<br>";
echo $error."<br>";
if(move_uploaded_file($tmp_name,'upload/'.$name)){
echo "文件上传成功!";
}
?>

Snipaste_2025-06-02_13-52-25

这里虽然显示了临时tmp的目录,但是当你去这个目录找这个文件的时候发现已经不再,而是在当前的upload目录里面。这里我上传的是一个txt的文本文档,所以这里mime的类型是:text/pain,下面的那个0表示没有错误信息

黑名单上传

这里如果上传了两个同名的文件,会将原先存在的文件进行一个替换的操作,解决办法就是对每个上传的文件使用时间戳重命名。

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
<?php

$name=$_FILES['file']['name'];
$type=$_FILES['file']['type'];
$size=$_FILES['file']['size'];
$tmp_name=$_FILES['file']['tmp_name'];
$error=$_FILES['file']['error'];

//上传文件后缀过滤 黑名单机制
$black_ext=array('php','asp','jsp','aspx');
//这里定义了一个数组的类型用来保存黑名单,到下面只需要验证上传的文件名是否在这个数组里面就可以了
$fenge = explode('.',$name);
//这里的explode函数用于将字符串分割成数组的函数。 它按照指定的分隔符来分割字符串,并返回一个数组。

$exts = end($fenge);
//end函数的意思是输出数组里面最后一个元素

if(in_array($exts,$black_ext)){
//这里的exts是被搜索的目标,black_ext是搜索的内容
//in_array这个函数就是如果在目标在数组里面,就返回true

echo '非法后缀文件'.$exts;
}else{
move_uploaded_file($tmp_name,'upload/'.$name);
echo '<script>alert("上传成功")</script>';
}

?>

上传php文件:

Snipaste_2025-06-02_14-07-59

上传txt文件:

Snipaste_2025-06-02_14-08-13

注意:如果对方使用的黑名单验证,然后在没有对php5这样的后缀进行验证,但是你访问这个文件的时候,是触发的下载,而不是解析,那么这种情况是根据你的环境(一般来说需要修改apache的配置文件)来说的,并不是你上传的问题。

白名单上传

1
2
3
4
5
6
7
8
9
10
$allow_ext=array('png','jpg','gif','jpeg');

$fenge = explode('.',$name);
$exts = end($fenge);
if(!in_array($exts,$allow_ext)){
echo '非法后缀文件'.$exts;
}else{
move_uploaded_file($tmp_name,'upload/'.$name);
echo '<script>alert("上传成功")</script>';
}

这里跟黑名单的区别就是在in_array这里进行一个取反的操作。然后这个名单里面填写可以上传的后缀就可以了。

MIME类型上传控制

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
<?php

$name=$_FILES['file']['name'];
$type=$_FILES['file']['type'];
$size=$_FILES['file']['size'];
$tmp_name=$_FILES['file']['tmp_name'];
$error=$_FILES['file']['error'];

// echo $name."<br>";
// echo $type."<br>";
// echo $size."<br>";
// echo $tmp_name."<br>";
// echo $error."<br>";
// if(move_uploaded_file($tmp_name,'upload/'.$name)){
// echo "文件上传成功!";
// }


// //上传文件后缀过滤 黑名单机制
// $black_ext=array('php','asp','jsp','aspx');
// //这里定义了一个数组的类型用来保存黑名单,到下面只需要验证上传的文件名是否在这个数组里面就可以了
// $fenge = explode('.',$name);
// //这里的explode函数用于将字符串分割成数组的函数。 它按照指定的分隔符来分割字符串,并返回一个数组。

// $exts = end($fenge);
// //end函数的意思是输出数组里面最后一个元素

// if(in_array($exts,$black_ext)){
// //这里的exts是被搜索的目标,black_ext是搜索的内容
// //in_array这个函数就是如果在目标在数组里面,就返回true

// echo '非法后缀文件'.$exts;
// }else{
// move_uploaded_file($tmp_name,'upload/'.$name);
// echo '<script>alert("上传成功")</script>';
// }

//上传文件后缀过滤 白名单机制
// $allow_ext=array('png','jpg','gif','jpeg');

// $fenge = explode('.',$name);
// $exts = end($fenge);
// if(!in_array($exts,$allow_ext)){
// echo '非法后缀文件'.$exts;
// }else{
// move_uploaded_file($tmp_name,'upload/'.$name);
// echo '<script>alert("上传成功")</script>';
// }

MIME文件类型过滤
$allow_type=array('image/png','image/jpg','image/jpeg','image/gif');
if(!in_array($type,$allow_type)){
echo '非法后缀文件';
}else{
move_uploaded_file($tmp_name,'upload/'.$name);
echo '<script>alert("上传成功")</script>';

}
?>

常见 MIME 类型示例

类型 典型扩展名 用途说明
text/plain .txt 纯文本文件
text/html .html, .htm HTML 网页文件
text/css .css 样式表文件
image/jpeg .jpg, .jpeg JPEG 图像
image/png .png PNG 透明背景图像
image/gif .gif GIF 动图
application/json .json JSON 数据格式
application/pdf .pdf PDF 文档
application/zip .zip 压缩文件
audio/mpeg .mp3 MP3 音频
video/mp4 .mp4 MP4 视频

Snipaste_2025-06-02_14-19-47

MIME类型就是上图数据包中的红框标识

文件管理

有些时候如果有目录穿越的话,有的可以访问有的不可以访问,这个是根据环境来判断的,有的中间件可以让你的访问局限在网站的根目录,linux上也可以通过selinux的安全上下文来限制访问的目录。

小迪的源码有问题,比如说我点击下级目录,这样在去看的时候全部都是文件了,本身存在的是文件夹,访问的时候都变成文件了,访问不了。

了解源码中的函数

  1. __DIR__
    PHP 魔术常量,表示当前脚本所在的绝对路径
    (例如:/var/www/html/your_folder
  2. realpath()
    • 将路径转换为标准化的绝对路径
    • 移除多余的.././
    • 解析符号链接
    • 检查路径是否存在
      (例如:/var/www/html/your_folder/../other/var/www/html/other
  3. 最终结果
    BASE_DIR 被定义为当前 PHP 文件所在目录的完整绝对路径
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
<?php
// 安全设置:限制访问的根目录(当前目录)
define('BASE_DIR', realpath(__DIR__));


// 获取要浏览的目录(进行安全过滤)
$dir = isset($_GET['dir']) ? $_GET['dir'] : '';
$current_path = realpath(BASE_DIR . DIRECTORY_SEPARATOR . $dir);
/*
这里两行代码的逻辑是$dir获取的是你点击的那个文件夹或者文件的名字。
然后$current_path是安全的根目录然后拼接你输入的那个文件夹或者文件的名字
*/


//DIRECTORY_SEPARATOR是一个返回跟操作系统相关的路径分隔符的php内置命令,在windows上返回\,而在linux或者类unix上返回/,就是这么个区别,通常在定义包含文件路径或者上传保存目录的时候会用到

// 验证路径是否在允许范围内
if ($current_path === false || strpos($current_path, BASE_DIR) !== 0) {
$current_path = BASE_DIR;
}
/*
这里是设置两个条件,然后只要有一个条件满足,就将当前值重置为安全目录的根目录。
第一个条件是:如果输入的目录是一个不存在的值,那么就返回false
第二个条件是:如果你是输入的值(/var/www)在根目录(/var/www/html)中就返回0,但是被第一个条件满足,因为在第一个条件看来,该值不存在
*/


// 处理返回上一级的链接
$parent_dir = dirname(str_replace(BASE_DIR, '', $current_path));
/*
假设你的根目录是:/var/www/html
你当前所在的目录是:/var/www/html/config/data
那么$current_path就是config/data
str_replace的操作就是把/var/www/html/替换为空,就得到config/data
然后在使用dirname获取上一级的目录,就得到config/data的上一级目录:config
从而得到返回上一级的参数。
*/

?>

<!DOCTYPE html>
<html>
<head>
<title>文件浏览器</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.file-item, .folder-item { padding: 5px; }
.folder-item { font-weight: bold; }
a { text-decoration: none; color: #0366d6; }
a:hover { text-decoration: underline; }
.current-path { background: #f0f0f0; padding: 10px; margin-bottom: 20px; }
</style>
</head>
<body>
<div class="current-path">
当前路径: <?= htmlspecialchars(str_replace(BASE_DIR, '/', $current_path)) ?: '/' ?>
<!-- 这里的意思就是将根目录替换为/,如果还是上面的config/data 那么就是得到 /config/data -->
</div>

<?php if ($current_path !== BASE_DIR): ?>
<div class="folder-item">
<a href="?dir=<?= urlencode($parent_dir) ?>">[返回上级目录]</a>
<!-- 返回上一级目录就调用前面用到的函数,然后进行url的编码,因为路径中可能会有中文 -->
</div>
<?php endif; ?>
<!-- 结束语句 -->

<?php
// 读取目录内容
$items = scandir($current_path);
//scandir() 函数返回一个数组,其中包含指定路径中的文件和目录。
//$current_path是安全的根目录然后拼接你输入的那个文件夹或者文件的名字
if ($items === false) {
die('无法读取目录内容');
}

foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;

$full_path = $current_path . DIRECTORY_SEPARATOR . $item;
//$full_path是获取当前的绝对路径+循环的这次的文件夹或者文件的名字
$relative_path = str_replace(BASE_DIR . DIRECTORY_SEPARATOR, '', $full_path);
//$relative_path去掉安全根目录,然后将斜杠或者反斜杠转换成当前系统能接收的方式。然后就可以获取当前循环的文件或文件夹的相对路径
//$item这个值就是当前每次循环的那个文件名或者文件夹的名字
if (is_dir($full_path)) {
// 也就是判断这个变量是否是一个文件夹,然后显示可点击的文件夹
echo '<div class="folder-item">';
echo '<a href="?dir=' . urlencode($relative_path) . '">📁 ' . htmlspecialchars($item) . '</a>';
//我要跳转点击$relative_path这个变量是去除了根目录之后的一个相对路径,然后显示的是这个相对路径下的当前这次循环的文件夹名字
echo '</div>';
} else {
// 显示文件
echo '<div class="file-item">';
echo '📄 ' . htmlspecialchars($item);
//只要文件夹不成立,那么就说明是文件,直接实体化输出文件名即可
echo '</div>';
}
}
?>
</body>
</html>

代码分块解释1

1
2
3
if ($current_path === false || strpos($current_path, BASE_DIR) !== 0) {
$current_path = BASE_DIR;
}

这段代码的作用,用于防止用户访问超出允许范围的目录。

1
2
3
if ($current_path === false || strpos($current_path, BASE_DIR) !== 0) {
$current_path = BASE_DIR;
}

1. 条件分解 - 双重安全验证

第一部分:$current_path === false

  • 含义:检查路径解析是否失败

  • 为什么需要

    1
    $current_path = realpath(BASE_DIR . DIRECTORY_SEPARATOR . $dir);
    • realpath() 函数在路径不存在或无效时会返回 false
    • 例如用户提交了无效路径:?dir=non_existent_folder
  • 作用:确保我们处理的路径是真实存在的物理路径

第二部分:strpos($current_path, BASE_DIR) !== 0

  • 含义:检查请求路径是否在允许的安全范围内
  • 详细解析
    • strpos($a, $b):查找 $b$a 中的位置(这里的意思是如果匹配到了一定是0(是从0,1,2这样开始算的),因为路径是绝对路径,如果输入的绝对路径可以匹配到说明没有问题)
    • !== 0:严格检查结果不是 0(即不在开头)
    • 相当于问:”$current_path 是否不以 BASE_DIR 开头?”
1
相当于一个路径:/var/www/html这个是我的BASE_DIR路径,如果你输入的值在我这里找不到,说明不在允许的安全范围内。
  • 安全意义

    1
    2
    3
    4
    5
    6
    7
    8
    // 合法路径示例:
    BASE_DIR = "/var/www/html"
    $current_path = "/var/www/html/documents" // 位置0 → 允许

    // 非法路径示例:
    BASE_DIR = "/var/www/html"
    $current_path = "/etc/passwd" // 位置false → 阻止
    $current_path = "/var/www" // 位置0但路径更短 → 阻止

2. 逻辑关系:|| (OR 操作符)

  • 任一条件为 true 时触发重置:
    1. 路径无效(=== false
    2. 路径不在安全范围内(!== 0
  • 相当于:”如果路径无效或越权,则重置”

3. 重置操作:$current_path = BASE_DIR

  • 作用:当检测到不安全路径时,强制将当前路径重置为安全根目录
  • 安全意义
    • 阻止目录遍历攻击(如 ?dir=../../etc
    • 防止访问系统敏感文件
    • 确保用户只能在沙箱内操作

情景1:正常访问子目录

1
2
3
4
5
6
7
8
9
10
11
用户请求: ?dir=documents

拼接路径: /var/www/html/documents

realpath() 返回: /var/www/html/documents (有效路径)

检查:
1. 不是false → 通过
2. strpos("/var/www/html/documents", "/var/www/html") → 返回0 → 通过

显示 documents 目录内容

情景2:恶意路径遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
用户请求: ?dir=../../../etc/passwd

拼接路径: /var/www/html/../../../etc/passwd

realpath() 返回: /etc/passwd (有效但危险!)

检查:
1. 不是false → 通过
2. strpos("/etc/passwd", "/var/www/html") → 返回false → 触发重置

重置为: /var/www/html

显示安全目录内容

情景3:无效路径

1
2
3
4
5
6
7
8
9
10
11
12
用户请求: ?dir=invalid_folder

拼接路径: /var/www/html/invalid_folder

realpath() 返回: false (目录不存在)

检查:
1. === false → 触发重置

重置为: /var/www/html

显示安全目录内容

代码分开解释2

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
<?php
// 读取目录内容
$items = scandir($current_path);
if ($items === false) {
die('无法读取目录内容');
}

foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;

$full_path = $current_path . DIRECTORY_SEPARATOR . $item;
$relative_path = str_replace(BASE_DIR . DIRECTORY_SEPARATOR, '', $full_path);


if (is_dir($full_path)) {
// 显示可点击的文件夹
echo '<div class="folder-item">';
echo '<a href="?dir=' . urlencode($relative_path) . '">📁 ' . htmlspecialchars($item) . '</a>';
echo '</div>';
} else {
// 显示文件
echo '<div class="file-item">';
echo '📄 ' . htmlspecialchars($item);
echo '</div>';
}
}

这块代码负责读取并显示当前目录的内容,并实现文件夹的导航功能。下面我将逐行详细解析:

1
2
3
4
5
6
<?php
// 读取目录内容
$items = scandir($current_path);
if ($items === false) {
die('无法读取目录内容');
}

1. 读取目录内容

  • scandir($current_path)
    • PHP 内置函数,扫描指定目录
    • 返回包含文件和子目录名称的数组
    • 数组包含特殊条目:.(当前目录)和..(上级目录)
  • 错误处理
    • 如果读取失败(如无权限),显示错误信息并终止脚本
1
2
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;

2. 过滤特殊目录项

  • 跳过...
    • 避免显示当前目录和上级目录链接
    • 因为已经单独实现了”返回上级目录”功能
    • 保持界面简洁
1
2
$full_path = $current_path . DIRECTORY_SEPARATOR . $item;
$relative_path = str_replace(BASE_DIR . DIRECTORY_SEPARATOR, '', $full_path);

3. 路径处理

  • $full_path

    • 构建文件/目录的完整物理路径
    • 使用DIRECTORY_SEPARATOR确保跨平台兼容性(Windows用\,Linux用/
  • $relative_path

    • 计算相对于安全根目录的路径

    • 用于生成URL参数

    • 示例:

      1
      2
      3
      BASE_DIR = "/var/www/html"
      $full_path = "/var/www/html/images/cats.jpg"
      $relative_path = "images/cats.jpg"
1
2
3
4
5
if (is_dir($full_path)) {
// 显示可点击的文件夹
echo '<div class="folder-item">';
echo '<a href="?dir=' . urlencode($relative_path) . '">📁 ' . htmlspecialchars($item) . '</a>';
echo '</div>';

4. 文件夹处理

  • is_dir()检测:判断是否为目录
  • 生成可点击链接
    • 使用文件夹图标📁
    • href="?dir=...":点击后URL参数更新为相对路径
    • 安全措施
      • urlencode():确保路径中的特殊字符正确传输
      • htmlspecialchars():防止XSS攻击,安全显示文件名
  • CSS样式folder-item类用于样式控制
1
2
3
4
5
6
7
8
    } else {
// 显示文件
echo '<div class="file-item">';
echo '📄 ' . htmlspecialchars($item);
echo '</div>';
}
}
?>

5. 文件处理

  • 非目录则视为文件
  • 直接显示
    • 使用文件图标📄
    • 不可点击(仅显示)
  • 安全措施
    • htmlspecialchars():防止恶意文件名执行脚本

完整工作流程示例:

假设目录结构:

1
2
3
4
5
6
7
/var/www/html (BASE_DIR)
├── documents/
│ └── report.pdf
├── images/
│ ├── cat.jpg
│ └── dog.png
└── index.php
  1. 首次访问

    • 显示:

      1
      2
      3
      [文件夹] documents → 链接到 ?dir=documents
      [文件夹] images → 链接到 ?dir=images
      [文件] index.php
  2. 点击”images”

    • URL变为:index.php?dir=images

    • 显示:

      1
      2
      3
      [返回上级目录]
      [文件] cat.jpg
      [文件] dog.png

关键安全特性:

  1. 路径隔离

    • 始终基于$current_path(已验证在安全范围内)
    • 使用相对路径生成链接
  2. 输出过滤

    • 文件名双重安全处理:

      1
      2
      urlencode() // URL安全
      htmlspecialchars() // HTML安全
  3. 目录遍历防护

    • 仅允许访问BASE_DIR及其子目录
    • 自动过滤...
  4. 错误处理

    • 目录读取失败时优雅退出

文件包含

1
2
3
4
include() 在错误发生后脚本继续执行
require() 在错误发生后脚本停止执行
include_once() 如果已经包含,则不再执行
require_once() 如果已经包含,则不再执行

正常的写法都是:

1
2
include "1.php";
这样的意思是包含了1.php这个文件

但是如果这里是一个可以接收变量的传参

1
2
3
4
5
6
include $_GET['page'];

这样写的话,那么如果我可以控制这个变量,那么就可以让他包含任意文件。
当然上面那种写法并不代表完全安全,因为如果1.php还包含了其他文件,如果1.php中的包含是通过变量进行接收的值,那么同样会出现任意文件包含。

文件包含可以配合文件上传进行一个组合拳,因为文件上传一般限制的很死,那么这个时候如果有文件包含你可以上传要给图片马,然后通过文件包含去包含这个文件,被包含的文件会被当做当前脚本语言去执行。也可以是txt或者其他格式。

文件管理

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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
<?php
// 安全设置:限制访问的根目录(当前目录)
define('BASE_DIR', realpath(__DIR__));
define('ALLOWED_EDIT_EXTENSIONS', ['txt', 'php', 'html', 'css', 'js', 'json', 'xml', 'md', 'log', 'ini', 'conf', 'env', 'htaccess']);
//定义了一个允许编辑的文件扩展名列表。这个列表用于限制用户只能编辑特定类型的文件(通常是文本文件),以防止用户编辑二进制文件(如图片、可执行文件等)可能导致的错误或安全问题。

// 获取要浏览的目录(进行安全过滤)
$dir = isset($_GET['dir']) ? $_GET['dir'] : '';
$current_path = realpath(BASE_DIR . DIRECTORY_SEPARATOR . $dir);

// 验证路径是否在允许范围内
if ($current_path === false || strpos($current_path, BASE_DIR) !== 0) {
$current_path = BASE_DIR;
}



// 处理返回上一级的链接
$parent_dir = dirname(str_replace(BASE_DIR, '', $current_path));

// 处理文件操作
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 文件编辑保存
if (isset($_POST['save_file'])) {
//edit_file:根目录/绝对路径/文件名
$edit_file = realpath(BASE_DIR . DIRECTORY_SEPARATOR . $_POST['file_path']);

// 验证文件路径
if ($edit_file && strpos($edit_file, BASE_DIR) === 0) {
$content = $_POST['file_content'];
if (file_put_contents($edit_file, $content)) {
//file_put_contents() 函数把一个字符串写入文件中
$message = '<div class="success">文件保存成功!</div>';
} else {
$message = '<div class="error">文件保存失败!</div>';
}
} else {
$message = '<div class="error">无效的文件路径!</div>';
}
}
} else if (isset($_GET['action'])) {
// 文件删除
if ($_GET['action'] === 'delete') {
$target = realpath(BASE_DIR . DIRECTORY_SEPARATOR . $_GET['file']);

// 验证路径
if ($target && strpos($target, BASE_DIR) === 0) {
if (is_dir($target)) {
// 删除目录(递归删除)
$deleted = deleteDirectory($target);
$message = $deleted ?
'<div class="success">目录删除成功!</div>' :
'<div class="error">目录删除失败!</div>';
} else {
// 删除文件
if (unlink($target)) {
$message = '<div class="success">文件删除成功!</div>';
} else {
$message = '<div class="error">文件删除失败!</div>';
}
}
} else {
$message = '<div class="error">无效的文件路径!</div>';
}
}
// 文件下载
else if ($_GET['action'] === 'download') {
$download_file = realpath(BASE_DIR . DIRECTORY_SEPARATOR . $_GET['file']);

// 验证文件路径
if ($download_file && strpos($download_file, BASE_DIR) === 0 && is_file($download_file)) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($download_file) . '"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($download_file));
flush();
readfile($download_file);
exit;
} else {
$message = '<div class="error">文件下载失败!无效的文件路径。</div>';
}
}
}

// 递归删除目录函数
function deleteDirectory($dir) {
if (!file_exists($dir)) {
//file_exists() 函数检查文件或目录是否存在。
return true;
}

if (!is_dir($dir)) {
return unlink($dir);
// 如果这里不是文件夹,就删除文件
}

foreach (scandir($dir) as $item) {
if ($item == '.' || $item == '..') {
continue;
}

if (!deleteDirectory($dir . DIRECTORY_SEPARATOR . $item)) {
return false;
}
}

return rmdir($dir);
}

// 检查文件是否可编辑
function isEditable($file) {
$ext = pathinfo($file, PATHINFO_EXTENSION);//返回文件名的后缀

/*
pathinfo函数以数组的形式返回关于文件路径的信息。

返回的数组元素如下:
[dirname]: 目录路径
[basename]: 文件名
[extension]: 文件后缀名
[filename]: 不包含后缀的文件名
*/


return in_array(strtolower($ext), ALLOWED_EDIT_EXTENSIONS);
//判断这个文件的后缀是否在最开始定义的数组常量里面
}
?>

<!DOCTYPE html>
<html>
<head>
<title>增强版文件浏览器</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f7fa;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0,0,0,0.1);
overflow: hidden;
}
header {
background: linear-gradient(135deg, #4b6cb7, #182848);
color: white;
padding: 20px;
text-align: center;
}
h1 {
margin: 0;
font-size: 28px;
}
.operations {
background: #f0f5ff;
padding: 15px;
display: flex;
justify-content: space-between;
border-bottom: 1px solid #d0d8e8;
}
.btn {
background: #4b6cb7;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
text-decoration: none;
display: inline-block;
}
.btn:hover {
background: #3a5795;
}
.btn-danger {
background: #e74c3c;
}
.btn-danger:hover {
background: #c0392b;
}
.btn-success {
background: #2ecc71;
}
.btn-success:hover {
background: #27ae60;
}
.message-container {
padding: 10px 20px;
}
.success {
background: #d4edda;
color: #155724;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
}
.current-path {
background: #e8f0fe;
padding: 15px 20px;
font-size: 16px;
border-bottom: 1px solid #d0d8e8;
}
.current-path span {
font-weight: bold;
color: #4b6cb7;
}
.file-list {
padding: 0;
margin: 0;
list-style: none;
}
.list-header {
display: grid;
grid-template-columns: 40px 2fr 1fr 150px 200px;
padding: 12px 20px;
background: #f8f9fa;
font-weight: bold;
border-bottom: 1px solid #eee;
}
.file-item, .folder-item {
display: grid;
grid-template-columns: 40px 2fr 1fr 150px 200px;
padding: 12px 20px;
border-bottom: 1px solid #f0f0f0;
align-items: center;
transition: background 0.2s;
}
.file-item:hover, .folder-item:hover {
background-color: #f9fbfd;
}
.folder-item {
font-weight: 600;
background-color: #f8fafd;
}
.item-icon {
font-size: 20px;
text-align: center;
}
.item-name {
font-weight: 500;
}
.item-actions {
display: flex;
gap: 8px;
}
.action-icon {
margin-right: 5px;
}
.editor-container {
padding: 20px;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
textarea {
width: 100%;
height: 400px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
font-family: monospace;
font-size: 14px;
line-height: 1.5;
}
.editor-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 15px;
}
.breadcrumb {
display: flex;
align-items: center;
padding: 0 20px 15px;
border-bottom: 1px solid #eee;
}
.breadcrumb a {
color: #4b6cb7;
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
.breadcrumb span {
margin: 0 8px;
color: #999;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>文件浏览器</h1>
<p>支持浏览、下载、删除、读取和编辑文件</p>
</header>

<div class="operations">
<div>
<a href="?dir=<?= urlencode($parent_dir) ?>" class="btn">
<span class="action-icon">⬆️</span> 返回上级目录
</a>
</div>
<div>
<a href="#" class="btn" onclick="alert('上传功能需要后端支持,请参考文档进行配置')">
<span class="action-icon">📤</span> 上传文件
</a>
</div>
</div>

<?php if ($message): ?>
<div class="message-container">
<?= $message ?>
</div>
<?php endif; ?>

<div class="current-path">
当前路径: <span><?= htmlspecialchars(str_replace(BASE_DIR, '/', $current_path)) ?: '/' ?></span>
</div>

<?php
// 显示文件编辑器(如果请求了编辑)
if (isset($_GET['action']) && $_GET['action'] === 'edit') {
$edit_file = realpath(BASE_DIR . DIRECTORY_SEPARATOR . $_GET['file']);

// 验证文件路径
if ($edit_file && strpos($edit_file, BASE_DIR) === 0 && is_file($edit_file) && isEditable($edit_file)) {
$content = htmlspecialchars(file_get_contents($edit_file));
$relative_path = str_replace(BASE_DIR . DIRECTORY_SEPARATOR, '', $edit_file);

echo '<div class="breadcrumb">
<a href="?dir=' . urlencode(dirname($relative_path)) . '">返回文件列表</a>
<span>›</span>
<strong>' . htmlspecialchars(basename($edit_file)) . '</strong>
</div>';

echo '<div class="editor-container">
<div class="editor-header">
<h2>编辑文件: ' . htmlspecialchars(basename($edit_file)) . '</h2>
</div>

<form method="POST">
<input type="hidden" name="file_path" value="' . htmlspecialchars($relative_path) . '">
<textarea name="file_content">' . $content . '</textarea>
<div class="editor-actions">
<a href="?dir=' . urlencode(dirname($relative_path)) . '" class="btn">取消</a>
<button type="submit" name="save_file" class="btn btn-success">保存更改</button>
</div>
</form>
</div>';

// 不显示文件列表
exit;
}
}
?>

<?php
// 显示文件内容(如果请求了查看)
if (isset($_GET['action']) && $_GET['action'] === 'view') {
$view_file = realpath(BASE_DIR . DIRECTORY_SEPARATOR . $_GET['file']);

// 验证文件路径
if ($view_file && strpos($view_file, BASE_DIR) === 0 && is_file($view_file) && isEditable($view_file)) {
$content = htmlspecialchars(file_get_contents($view_file));
$relative_path = str_replace(BASE_DIR . DIRECTORY_SEPARATOR, '', $view_file);

echo '<div class="breadcrumb">
<a href="?dir=' . urlencode(dirname($relative_path)) . '">返回文件列表</a>
<span>›</span>
<strong>' . htmlspecialchars(basename($view_file)) . '</strong>
</div>';

echo '<div class="editor-container">
<div class="editor-header">
<h2>查看文件: ' . htmlspecialchars(basename($view_file)) . '</h2>
<a href="?action=edit&file=' . urlencode($relative_path) . '&dir=' . urlencode($dir) . '" class="btn">编辑</a>
</div>

<pre style="background: #f8f9fa; padding: 20px; border-radius: 5px; border: 1px solid #eee; max-height: 500px; overflow: auto;">' . $content . '</pre>

<div class="editor-actions">
<a href="?dir=' . urlencode(dirname($relative_path)) . '" class="btn">返回</a>
</div>
</div>';

// 不显示文件列表
exit;
}
}
?>

<div class="list-header">
<div class="item-icon"></div>
<div class="item-name">名称</div>
<div>类型</div>
<div>大小</div>
<div>操作</div>
</div>

<ul class="file-list">
<?php
// 读取目录内容
$items = scandir($current_path);
if ($items === false) {
die('无法读取目录内容');
}

// 先显示目录
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;

$full_path = $current_path . DIRECTORY_SEPARATOR . $item;
$relative_path = str_replace(BASE_DIR . DIRECTORY_SEPARATOR, '', $full_path);

if (is_dir($full_path)) {
$size = '-';
$type = '文件夹';

echo '<li class="folder-item">';
echo '<div class="item-icon">📁</div>';
echo '<div class="item-name"><a href="?dir=' . urlencode($relative_path) . '">' . htmlspecialchars($item) . '</a></div>';
echo '<div>' . $type . '</div>';
echo '<div>' . $size . '</div>';
echo '<div class="item-actions">';
echo '<a href="?action=delete&file=' . urlencode($relative_path) . '&dir=' . urlencode($dir) . '" class="btn btn-danger" onclick="return confirm(\'确定要删除这个文件夹吗?此操作不可恢复!\')">删除</a>';
echo '</div>';
echo '</li>';
}
}

// 再显示文件
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;

$full_path = $current_path . DIRECTORY_SEPARATOR . $item;
$relative_path = str_replace(BASE_DIR . DIRECTORY_SEPARATOR, '', $full_path);

if (!is_dir($full_path)) {
$size = filesize($full_path);
$type = pathinfo($item, PATHINFO_EXTENSION) . ' 文件';

// 格式化文件大小
if ($size < 1024) {
$size = $size . ' B';
} elseif ($size < 1048576) {
$size = round($size / 1024, 2) . ' KB';
} else {
$size = round($size / 1048576, 2) . ' MB';
}

echo '<li class="file-item">';
echo '<div class="item-icon">📄</div>';
echo '<div class="item-name">' . htmlspecialchars($item) . '</div>';
echo '<div>' . $type . '</div>';
echo '<div>' . $size . '</div>';
echo '<div class="item-actions">';
echo '<a href="?action=download&file=' . urlencode($relative_path) . '" class="btn">下载</a>';
echo '<a href="?action=delete&file=' . urlencode($relative_path) . '&dir=' . urlencode($dir) . '" class="btn btn-danger" onclick="return confirm(\'确定要删除这个文件吗?此操作不可恢复!\')">删除</a>';

if (isEditable($full_path)) {
echo '<a href="?action=view&file=' . urlencode($relative_path) . '&dir=' . urlencode($dir) . '" class="btn">查看</a>';
echo '<a href="?action=edit&file=' . urlencode($relative_path) . '&dir=' . urlencode($dir) . '" class="btn">编辑</a>';
}

echo '</div>';
echo '</li>';
}
}
?>
</ul>
</div>

<script>
// 确认删除操作
function confirmDelete() {
return confirm('确定要删除这个文件/文件夹吗?此操作不可恢复!');
}
</script>
</body>
</html>

Snipaste_2025-06-03_07-28-16

删除流程:

deepseek_mermaid_20250603_ededd0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
A[开始删除目录] --> B{是文件?}
B -->|是| C[删除文件]
B -->|否| D[遍历目录内容]
D --> E[获取第一项]
E --> F{是 . 或 ..?}
F -->|是| G[跳过]
F -->|否| H[递归删除该项]
H --> I{删除成功?}
I -->|是| J[获取下一项]
I -->|否| K[返回失败]
J --> E
G --> J
K --> L[结束]
J -->|所有项处理完| M[删除空目录]
M --> N{删除成功?}
N -->|是| O[返回成功]
N -->|否| P[返回失败]

小迪的文件管理

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
<?php
ini_set('open_basedir',__DIR__);
$path=$_GET['path'] ?? './';
$action = isset($_GET['a'])?$_GET['a']:'';
$path = isset($_GET['path'])?$_GET['path']:'.';
if(is_file($path))
{
//获得文件名
$file = basename($path);
//获得路径
$path = dirname($path);
}
//判断,不是目录
elseif(!is_dir($path))
{
echo '我只会吃瓜!';
}
function getlist($path){
$hd=opendir($path);
while(($file_name=readdir($hd) )!== false){
if($file_name != '.' && $file_name != '..'){
$file_path = "$path/$file_name";
$file_type = filetype($file_path);
}
$list[$file_type][] = array( //$file_type = dir 和 file $list['dir'] 和 $list['file']
'file_name'=>$file_name, //文件名存储键值file_name
'file_path'=>$file_path, //文件路径存储键值file_path
'file_size'=>round(filesize($file_path)/1024), //通过换算文件大小存储键值file_path
'file_time'=>date('Y/m/d H:i:s',filemtime($file_path)), //获取文件时间并存储键值file_path
);

}
closedir($hd);
return $list;
}

$list=getlist($path);


//接受方法 判断是怎么操作
//echo $action;
switch ($action){
case 'del':
unlink($file);
//$cmd="del $file";
//system($cmd);
//echo $cmd;
break;
case 'down':
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"" . $file . "\"");
header("Content-Length: " . filesize($file));
readfile($file);
break;
case 'edit':
$content=file_get_contents($file);
echo '<form name="form1" method="post" action="">';
echo "文件名:".$file."<br>";
echo "文件内容:<br>";
echo '<textarea name="code" style="resize:none;" rows="100" cols="100"">'.$content.'</textarea><br>';
echo '<input type="submit" name="submit" id="submit" value="提交">';
echo '</form>';
break;
}

//检测编辑后提交的事件 进入文件重新写入
if(isset($_POST['code'])){
$f=fopen("$path/$file",'w+');
fwrite($f,$_POST['code']);
fclose($f);
}




?>

<table width="100%" style="font-size: 10px;text-align: center;">
<tr>
<th>图标</th>
<th>名称</th>
<th>日期</th>
<th>大小</th>
<th>路径</th>
<th>操作</th>
</tr>
<?php foreach ($list['dir'] as $v): ?>
<tr>
<td><img src="./img/list.png" width="20" height="20"></td>
<td><?php echo $v['file_name']?></td>
<td><?php echo $v['file_time']?></td>
<td>-</td>
<td><?php echo $v['file_path']?></td>
<td><a href="?path=<?php echo $v['file_path']?>">打开</a></td>
</tr>
<?php endforeach;?>

<?php foreach ($list['file'] as $v): ?>
<tr>
<td><img src="./img/file.png" width="20" height="20"></td>
<td><?php echo $v['file_name']?></td>
<td><?php echo $v['file_time']?></td>
<td><?php echo $v['file_size']?></td>
<td><?php echo $v['file_path']?></td>
<td>
<a href="?a=edit&path=<?php echo $v['file_path']?>">编辑</a>
<a href="?a=down&path=<?php echo $v['file_path']?>">下载</a>
<a href="?a=del&path=<?php echo $v['file_path']?>">删除</a>
</td>

</tr>
<?php endforeach;?>


</table>

思考这些代码中出现的安全问题

  • 文件包含(包含的文件,以及被包含的文件中再次引用的包含都不能用变量进行传输)以及目录的读取,如果没有做严格的安全限制,那么就会出现目录穿越(可以通过php.ini(define(‘BASE_DIR’, realpath(DIR));)中的安全设置,强制用户只能读取当前脚本的目录以及子目录)的问题。比如:./../这样可以穿越目录。然后就可以进行文件目录的一个变量,文件包含上面也说过,如果你的include后面的值是一个可以控制的变量,那么就会出现,任何可以被读取到的文件都会被当作网站的脚本语言执行。
  • 文件上传:可以让攻击者直接上传恶意文件。(安全的思路,首先你要取得文件的最后一个后缀,这个过程一定要严谨,然后在使用白名单,对这个后缀进行一个验证)
  • 文件下载:可以下载任意的文件。需要对用下载的用户进行鉴权,如果没有权限下载,就不能允许下载。
  • 文件删除:可以删除任意的文件。同文件下载,需要对用户进行鉴权。这个鉴权需要严谨。
1
同时这里使用的代码中的函数进行文件上传,文件的删除和文件的下载这样的操作,但是有些函数是系统的函数,也就是说不是通过代码实现的删除功能, 而是直接通过系统的命令进行删除,如果这里没有做严格的控制同样会出现一个命令执行的问题。