文件上传漏洞:从原理到防御,构建Web安全第一道防线
1. 从一次“意外”的文件上传说起那天我正在帮一个朋友检查他刚上线的个人博客。他兴致勃勃地告诉我后台加了个很酷的功能可以上传头像和文章配图。出于职业习惯我随手试了试上传了一张普通的JPG图片一切正常。然后我鬼使神差地把一个文本文件的后缀名从.txt改成了.jpg再次点击上传——页面竟然也显示“上传成功”并且返回了一个可以直接访问的URL。我心里“咯噔”一下这可不是什么好兆头。我立刻访问那个链接服务器诚实地把那个文本文件的内容原原本本地展示在了浏览器里。这个看似微小的“意外”实际上暴露了一个非常典型且高危的Web安全漏洞文件上传漏洞。对于刚入门安全测试的朋友或是负责网站开发的工程师来说理解并防范这个漏洞是构建安全防线的第一课。它不复杂但威力巨大攻击者一旦利用成功轻则篡改网页、挂黑页重则获取服务器控制权导致数据泄露甚至服务瘫痪。今天我们就抛开那些复杂的理论从一个真实的视角手把手拆解文件上传漏洞的成因、利用手法以及最关键的——如何从开发层面就把它堵死。2. 漏洞的本质为什么服务器会“吃”下不该吃的东西文件上传功能本身不是漏洞漏洞出在服务端对上传文件的处理逻辑存在缺陷。我们可以把服务器想象成一个严格的安检员它的职责是检查每一个想要进入“服务器内部区域”通常是某个Web目录的“访客”文件。一个健壮的上传逻辑应该进行多维度、深层次的检查。而存在漏洞的程序其安检流程往往存在以下一个或多个疏漏2.1 只认“外套”不认人仅检查客户端提交的数据这是最原始、也最低级的错误。在HTTP协议中文件上传时浏览器会在请求头里附带一个Content-Type字段比如image/jpeg同时文件名如shell.jpg也会一并提交。有些服务端程序仅仅检查这些由客户端浏览器发送过来的、完全可控的信息。攻击者可以轻松伪造这些信息。使用Burp Suite、Postman等工具拦截上传请求直接将Content-Type改为image/jpeg或者将文件名改为shell.jpg.php就能绕过这种检查。服务器仅仅因为文件“自称”是图片就放行了这好比安检只看了访客自己填的申请表就放行毫无安全性可言。2.2 简单的“后缀名过滤”及其绕过意识到不能信任客户端后开发者开始在后端检查文件后缀名。他们会维护一个“白名单”如.jpg,.png,.gif或“黑名单”如.php,.jsp,.asp。黑名单机制非常不可靠因为可执行脚本的后缀变种太多.php5,.phtml,.phps,.php7等防不胜防。即使是白名单机制如果实现不当也存在绕过可能大小写绕过在Linux/Unix系统上.PHP、.Php和.php是不同文件。如果检查逻辑是简单的字符串匹配str “.php”就可能被绕过。双写后缀绕过如果过滤逻辑是简单地删除字符串.php那么文件名shell.php.jpg经过过滤后可能变成shell.jpg但某些不严谨的删除逻辑可能只执行一次导致shell.phphpp变成shell.php。后缀名后添加特殊字符在某些解析环境下shell.php.末尾有点、shell.php末尾有空格或shell.php%00空字节截断在特定PHP版本中可能导致服务器解析文件时识别出.php后缀。空字节截断%00是历史上一个经典漏洞虽然现代PHP版本默认修复但在老旧系统或特定配置下仍需警惕。利用解析特性配合其他漏洞这是更高级的利用方式。例如如果服务器是Nginx且配置不当可能将shell.jpg/.php这样的路径解析为PHP文件。或者如果Apache服务器对.php.xxx这样的文件设置了AddType解析规则也可能导致非.php后缀的文件被当作PHP执行。2.3 缺失的“文件内容”检查这是最关键的防线。一个图片文件无论它叫什么名字其文件内容的二进制结构都有特定的标识即“文件头”或“魔术字节”。例如JPEG:FF D8 FF E0PNG:89 50 4E 47GIF:47 49 46 38服务端应该读取上传文件的前几个字节验证其是否与宣称的文件类型通过后缀名判断相符。如果缺少这一步攻击者可以将一个PHP木马文件的前面加上图片的文件头制作成一个“图片马”。上传后它可能通过白名单检查因为后缀是.jpg在访问时如果服务器没有做内容检查它就会被当作图片直接输出二进制内容不执行。但是如果存在本地文件包含漏洞配合服务器可能会将这个“图片马”作为PHP代码包含并执行危害极大。2.4 不安全的存储路径与访问权限即使文件本身检查通过了如果存储路径和权限设置不当也会引入风险。存储路径可预测/可遍历如果上传后的文件路径是按时间、用户名等简单规则生成且文件名不变攻击者容易猜测出其他用户的文件路径。更严重的是如果保存文件名时未重命名攻击者可能上传一个覆盖已有系统文件的恶意文件需有权限。文件被存储在Web可访问目录下这是执行恶意代码的前提。上传的文件必须能够通过Web URL直接或间接访问到。如果服务器将上传文件存储在Web根目录之外即使上传了脚本文件攻击者也无法触发执行。权限问题上传目录如果具有执行权限如755中的x那么脚本文件一旦被放入就可能被执行。3. 实战演练手动探测与利用文件上传点理解了原理我们最好在一个合法的测试环境如DVWA、Upload Labs等靶场中进行实践。这里以手动测试思路为例不涉及具体靶场操作。3.1 信息收集与初步测试首先找到网站的上传功能点如图片上传、附件上传、头像修改等。正常上传先上传一个合规的图片文件如test.jpg观察整个过程。请求是否被前端JavaScript校验可以尝试禁用JS或修改前端代码绕过。上传成功后文件被存储在哪里浏览器返回的路径或提示信息是什么是直接返回了完整的http://site.com/uploads/xxx.jpg链接还是一个相对路径这个路径的规律是什么例如/uploads/20240527/xxxxxx.jpg。文件是否被重命名是原文件名保存还是被重命名为随机字符串如UUID随机化命名是很好的安全实践。探测过滤规则改后缀尝试上传一个将test.php改名为test.jpg的文件。如果被拦截提示信息是什么“文件类型不允许”还是“文件内容不安全”改Content-Type如果改后缀被拒用代理工具拦截请求将Content-Type从application/octet-stream改为image/jpeg再次发送看是否绕过。大小写、双写、点空格测试尝试test.Phptest.php.jpgtest.php.test.php注意空格。3.2 构造Payload与上传假设我们初步判断服务器只做了黑名单过滤且过滤不严我们尝试上传一个简单的WebShell。WebShell是一段驻留在Web服务器上的脚本代码攻击者可以通过Web请求与之交互从而在服务器上执行命令。一个最简单的PHP一句话木马如下?php eval($_POST[cmd]);?这段代码的意思是执行通过POST参数cmd传递过来的任意PHP代码。符号用于抑制错误信息避免暴露。我们的任务就是让服务器以.php后缀保存这个文件。如果直接上传shell.php被拦截可以尝试shell.php5(利用其他PHP处理器后缀)shell.phtml(某些配置下可执行)shell.php.jpg(配合解析漏洞或特殊处理逻辑)在文件开头添加GIF文件头GIF89a制作图片马shell.gif然后利用文件包含漏洞执行。重要提示所有测试必须在自己拥有完全控制权的实验环境如本地虚拟机搭建的靶场中进行。未经授权对任何线上系统进行渗透测试是非法行为将面临法律制裁。3.3 访问与验证上传成功后我们需要访问这个文件来验证是否利用成功。根据上传成功后返回的路径拼接出完整的URL。使用浏览器访问该URL。如果是一个图片马浏览器可能会显示图片损坏如果是一个脚本文件可能会显示空白页代码已执行但无输出、代码本身说明未解析被当作文本显示了或报错。使用中国蚁剑、冰蝎、C刀等WebShell管理工具进行连接测试。以中国蚁剑为例在添加Shell时填写URL地址我们上传的WebShell路径连接密码即我们木马中定义的参数名如上面的cmd编码类型一般选择default。点击连接如果成功则会列出服务器上的目录结构此时就获得了该Web目录下的文件管理权限。4. 防御之道构建多层次的文件上传安全方案知道了怎么攻才能更好地防。一个健壮的文件上传功能应该像洋葱一样层层设防。4.1 前端校验仅作为用户体验优化前端通过JavaScript检查文件后缀、大小可以即时给用户反馈但绝不能作为安全凭据。因为攻击者可以轻易绕过前端直接发送HTTP请求。这个环节的价值在于减少无效请求对服务器的压力并提升正常用户的体验。4.2 后端校验安全的核心防线这是所有防御工作的重心必须在服务器端进行。白名单策略严格定义允许上传的文件类型后缀列表如.jpg,.jpeg,.png,.gif。只接受列表内的类型其他一律拒绝。这比黑名单有效得多。文件内容检查MIME类型检查检查$_FILES[‘file’][‘type’]但同样不可全信需结合其他方法。文件头/魔术字节校验这是最可靠的方法之一。用编程语言读取文件的前几个字节判断其是否与宣称的后缀匹配。例如一个后缀是.jpg的文件其文件头必须是FF D8 FF E0或FF D8 FF E1。图像二次渲染/重采样对于图片文件最彻底的安全处理方式是使用GD库、ImageMagick等库将上传的图片读取到内存中创建一个新的图片资源再保存到磁盘。这个过程会剥离所有可能嵌入在图片元数据如EXIF中的恶意代码。即使上传的是图片马经过二次渲染后附加的PHP代码也会被完全清除只留下纯粹的图像数据。文件重命名上传后不要使用用户上传时的原始文件名。应采用不可预测的命名规则如“时间戳随机字符串白名单后缀”20240527123456_abc123def.jpg。这样即使攻击者上传了恶意文件也无法知道最终访问的URL是什么。控制存储路径将上传文件存储在Web根目录之外的独立目录。这样用户无法通过URL直接访问到这些文件。通过一个专门的、安全的“文件下载/访问脚本”来提供文件服务。这个脚本负责验证用户权限、记录日志并读取文件内容输出给浏览器。例如用户访问/download.php?id123脚本根据id从数据库找到安全的存储路径在确认用户有权访问后才用readfile()函数输出图片内容。这样即使存储目录里有非图片文件也无法被直接执行。4.3 服务器配置加固应用层防御之外系统层也需要加固。目录权限上传目录的权限应设置为755所有者可读可写可执行其他用户只读可执行或更严格的750。最关键的是移除上传目录的脚本执行权限。在Nginx中可以在location配置中添加location ~ ^/uploads/.*\.(php|php5|jsp)$ { deny all; }。在Apache中可以在上传目录的.htaccess文件中添加RemoveHandler .php .php5 .phtml等指令。使用安全的中间件确保Web服务器Nginx/Apache、编程语言解释器PHP/Python等均为最新稳定版本及时修补已知的解析漏洞。WAFWeb应用防火墙部署WAF可以拦截一些通用的、特征明显的文件上传攻击Payload作为最后一道补充防线。5. 开发中的常见“坑”与最佳实践在实际开发中我见过太多因为“想当然”而引入漏洞的案例。这里分享几个血泪教训“我用了框架的上传组件应该安全了吧”框架的组件通常提供了基础的安全方法但如何配置和使用它责任在开发者。你必须显式地设置白名单、启用内容检查、配置存储路径。默认配置往往不是最安全的。“我只允许登录用户上传所以风险低。”权限校验和上传安全是两个维度。认证用户也可能被劫持会话固定、XSS盗取Cookie或者内部人员作案。上传安全机制必须独立于业务权限对所有上传者一视同仁。“文件已经重命名了后缀也控制了没问题。”如果缺少内容检查攻击者上传一个“图片马”你的重命名规则可能把它存为randombg.jpg。当网站其他地方存在“本地文件包含漏洞”时攻击者可以包含这个jpg文件由于文件头是图片PHP解析器可能会跳过文件头直接执行后面的PHP代码导致漏洞被组合利用。“我们用了云存储OSS文件不落地。”云存储服务如阿里云OSS、腾讯云COS本身很安全但你的服务端代码在将文件转发到OSS之前仍然在临时目录接收了文件。这个临时接收的过程如果处理不当如直接执行了move_uploaded_file到某个可访问目录同样存在风险。正确的做法是在内存或临时文件中完成所有安全检查白名单、文件头、病毒扫描后再调用云存储的SDK上传。一个相对完整的上传函数伪代码思路function safeUpload($fileInputName) { // 1. 检查上传过程是否出错 if ($_FILES[$fileInputName][error] ! UPLOAD_ERR_OK) { die(上传失败); } // 2. 定义白名单 $allowedExts [jpg, jpeg, png, gif]; $allowedMimes [image/jpeg, image/png, image/gif]; // 3. 获取原始信息 $tmpPath $_FILES[$fileInputName][tmp_name]; $originalName $_FILES[$fileInputName][name]; $clientMime $_FILES[$fileInputName][type]; // 4. 检查后缀名白名单 $ext strtolower(pathinfo($originalName, PATHINFO_EXTENSION)); if (!in_array($ext, $allowedExts)) { die(文件类型不允许); } // 5. 检查MIME类型白名单 if (!in_array($clientMime, $allowedMimes)) { die(文件类型不允许); } // 6. 检查文件头真实类型 $fileInfo finfo_open(FILEINFO_MIME_TYPE); $realMime finfo_file($fileInfo, $tmpPath); finfo_close($fileInfo); if (!in_array($realMime, $allowedMimes)) { die(文件内容类型不匹配); } // 7. 图片二次渲染以GD库为例 $image null; switch ($realMime) { case image/jpeg: $image imagecreatefromjpeg($tmpPath); break; case image/png: $image imagecreatefrompng($tmpPath); break; case image/gif: $image imagecreatefromgif($tmpPath); break; default: die(不支持的图片格式); } if (!$image) { die(图片文件损坏或无法处理); } // 8. 生成安全的新文件名和存储路径Web不可直接访问 $newFilename uniqid() . _ . bin2hex(random_bytes(8)) . . . $ext; $savePath /var/www/secure_upload_dir/ . $newFilename; // Web目录外 // 9. 保存渲染后的新图片 switch ($ext) { case jpg: case jpeg: imagejpeg($image, $savePath, 90); // 保存为JPEG质量90% break; case png: imagepng($image, $savePath, 9); // 保存为PNG压缩级别9 break; case gif: imagegif($image, $savePath); break; } imagedestroy($image); // 10. 将文件信息如$newFilename存入数据库并通过安全的下载脚本提供访问 return $newFilename; }文件上传漏洞是一个“细节决定成败”的典型。它考验的不是高深的技术而是开发者严谨的安全意识和防御体系思维。每一次上传操作都应该像对待一个未知的包裹经过多道安检确认无害后再放入指定的安全区域。对于安全测试者而言掌握它的原理和利用方式则是打开Web安全大门的一把基础且关键的钥匙。在自家靶场里多练练手把各种绕过手法和防御措施都体验一遍当你再回头看自己写的代码时自然就知道该在哪里加上那把牢固的锁了。

相关新闻

Git冲突解决:掌握--ours与--theirs策略,高效处理合并与变基

Git冲突解决:掌握--ours与--theirs策略,高效处理合并与变基

1. 从“合并地狱”到优雅解决&#xff1a;理解冲突的本质在团队协作开发中&#xff0c;Git 合并或变基时遇到冲突&#xff0c;几乎是每个开发者都会经历的“必修课”。面对满屏的<<<<<<< HEAD、和>>>>>>> feature-branch标记&#x…

2026/10/9 7:13:00 阅读更多 →
SSH公钥认证失败排查指南:从Permission denied到无密码登录

SSH公钥认证失败排查指南:从Permission denied到无密码登录

1. 问题现象与核心原因剖析“Permission denied (publickey,gssapi-keyex,gssapi-with-mic)”这个错误提示&#xff0c;对于经常通过SSH管理远程服务器的运维工程师和开发者来说&#xff0c;绝对是一个高频“拦路虎”。它不像密码错误那样直接&#xff0c;而是告诉你&#xff1…

2026/10/5 9:25:09 阅读更多 →
SSH公钥认证失败:Permission denied排查与修复指南

SSH公钥认证失败:Permission denied排查与修复指南

1. 问题现象与核心场景剖析“Permission denied (publickey,gssapi-keyex,gssapi-with-mic)”这个错误提示&#xff0c;对于任何需要通过SSH远程管理Linux服务器的开发者、运维工程师或者学生来说&#xff0c;都堪称是“入门第一课”的经典绊脚石。你信心满满地在终端敲下ssh u…

2026/10/7 5:16:00 阅读更多 →

最新新闻

油气管道SCADA系统:三代沿革、架构选型与数字管道落地实践

油气管道SCADA系统:三代沿革、架构选型与数字管道落地实践

简介&#xff1a;这份PPT面向油气储运、自动化及工业控制领域的学习者与工程技术人员&#xff0c;系统讲解油气管道SCADA系统及过程控制的核心知识&#xff0c;帮助读者建立从数据采集、监视控制到数字化管道建设的整体认知框架。资源为单个PPT文件&#xff0c;压缩包约5.29MB&…

2026/10/11 14:49:45 阅读更多 →
人形机器人运动控制入门:Xbotics运控全景从行走到全身操作

人形机器人运动控制入门:Xbotics运控全景从行走到全身操作

【免费下载链接】Xbotics-Embodied-Guide Xbotics 社区具身智能学习指南&#xff1a;我们把“具身综述→学习路线→仿真学习→开源实物→人物访谈→公司图谱”串起来&#xff0c;帮助新手和实战者快速定位路径、落地项目与参与开源。 项目地址&#xff1a; https://gitcode.com…

2026/10/11 14:49:45 阅读更多 →
基于AB 1756 PLC的冲渣池泵阀联锁控制与上位机画面整合实践

基于AB 1756 PLC的冲渣池泵阀联锁控制与上位机画面整合实践

说到钢铁厂里的冲渣池&#xff0c;很多搞自动化的人第一反应是工艺简单&#xff0c;不就是几台泵、几个阀、再配个液位嘛。可真到了现场你会发现&#xff0c;蒸汽弥漫、水温高、池面波动快&#xff0c;泵空转、水池溢流、阀后憋压&#xff0c;哪一件都不是小事。我最近完成了一…

2026/10/11 14:49:45 阅读更多 →
高校食堂点评系统实战:低样本下的评分排序与推荐算法

高校食堂点评系统实战:低样本下的评分排序与推荐算法

简介&#xff1a;这是一套面向高校学生与教职员工、基于PHP开发的食堂菜品点评Web应用源码&#xff0c;适合Web开发初学者、课程设计或毕业设计参考者&#xff0c;用于实现菜品打分、评论互动、食堂信息展示与数据分析等功能。压缩包共146个文件&#xff0c;约43.18MB&#xff…

2026/10/11 14:49:45 阅读更多 →
绝缘子缺陷识别数据集:COCO转YOLOv8训练实战与避坑指南

绝缘子缺陷识别数据集:COCO转YOLOv8训练实战与避坑指南

简介&#xff1a;面向电力巡检与视觉检测方向的开发者&#xff0c;这份绝缘子缺陷识别数据集提供1598张真实巡检图片&#xff0c;并完成COCO格式标注&#xff0c;可支持训练目标检测或实例分割模型&#xff0c;识别光盘损坏、绝缘子本体及污闪三类典型异常&#xff0c;标注正确…

2026/10/11 14:49:45 阅读更多 →
OpenClaw网关层架构解析:从请求生命周期到流量治理核心设计

OpenClaw网关层架构解析:从请求生命周期到流量治理核心设计

OpenClaw 拆完整个网关层之后&#xff0c;我发现真正决定线上稳定性的往往不是业务代码&#xff0c;而是流量进来之后的前 100 毫秒。这次写一篇 OpenClaw 技术架构解析-网关层&#xff08;上&#xff09;&#xff0c;主要讲讲接入层的设计定位、核心组件拆解、一次完整请求的生…

2026/10/11 14:48:45 阅读更多 →

日新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介&#xff1a;基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码&#xff0c;面向计算机相关专业课程设计与期末大作业学生&#xff0c;以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程&#xff0c;…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程&#xff1a;键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化&#xff0c;十个新手有八个栽在"往输入框里填东西"这件事上&#xff1a;要么填不进去&#xff0c;要么填了一半&#xff0c;要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程&#xff1a;阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀&#xff1a;什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面&#xff0c;跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介&#xff1a;基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码&#xff0c;面向计算机相关专业课程设计与期末大作业学生&#xff0c;以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程&#xff0c;…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程&#xff1a;键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化&#xff0c;十个新手有八个栽在"往输入框里填东西"这件事上&#xff1a;要么填不进去&#xff0c;要么填了一半&#xff0c;要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程&#xff1a;阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀&#xff1a;什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面&#xff0c;跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →