做数据标注平台的部署和维护,最怕听到的一句话就是:“这个系统好像被人扫到了。”我接手过的数据标注项目里,doccano 出现的频率相当高,它界面简洁、标注任务配置灵活,支持文本分类、序列标注、序列到序列等常见任务,团队协作功能也算够用。但越多人用,就越要正视一个问题:很多团队把它部署好就丢在生产环境,默认配置跑到底,而它恰恰是攻击者眼里“高价值低防护”的靶子——上面不仅有标注数据,还有项目配置、用户账号、API 接口,一旦被打穿,轻则数据泄露,重则整个标注流水线瘫痪。
这篇指南不是安全理论课,而是基于我在真实项目里对 doccano 做安全加固的完整记录。围绕 SQL 注入与 XSS 这两类最常见的 Web 攻击,从原理拆解到 Django 层面的具体防护写法,再到数据库账号权限、Nginx 层兜底、部署后的自检清单,能直接抄作业。适合正在用或准备用 doccano 做标注平台的开发者、数据团队负责人,以及所有想把开源工具用得稍微体面一点的人。
1. 安全基线认知:doccano 的攻防面与纵深防御思路
1.1 两类攻击在标注平台上的真实风险
很多人觉得 SQL 注入和 XSS 是老生常谈,但在标注平台这种“多人协作 + 富文本内容 + 复杂查询”的场景里,它们的杀伤力会被明显放大。
先看 SQL 注入。doccano 的底层是 Django 框架,ORM 的默认参数化机制能挡住大部分低级拼接,但安全漏洞往往出在“绕过 ORM”的地方。项目里常见的高危点包括:编写自定义查询时直接使用connection.cursor()拼接条件;大量使用extra()或RawSQL处理标注结果统计;搜索功能里对用户输入做了奇怪的字符串处理后塞进filter()的参数;导入导出模块处理 JSON 或 CSV 时把字段名、表名参数化了——这种属于 Django 里最容易踩的坑,因为table_name这类标识符没法用占位符,一旦来自前端配置,就变成了注入点。
再看 XSS。标注平台天然是高危场景,因为大量用户输入的内容会被原样存进数据库,再被其他协作者、审核员或管理员在浏览器中查看。一个标注者粘贴了一段带有<script>标签的文本,或者一条注释里嵌入了事件属性,如果渲染时没有转义或过滤,这段脚本就会在另一个用户的浏览器里执行。攻击者可以利用这个通道窃取会话 Cookie、模拟管理员操作、读取标注界面下的敏感数据,甚至配合 CSRF 把项目配置改掉。比起传统网站的一次性攻击,标注平台里 XSS 的“潜伏期”更长——数据就静静躺在数据库里,谁打开谁中招。
1.2 纵深防御的设计逻辑
面对这两类风险,我的思路不是“只修代码”,而是把防线拆成三层:应用层挡输入、业务层管权限、基础设施层做兜底。
- 应用层:规范所有数据库访问路径,强制走 ORM 参数化查询;对用户输入做白名单校验;模板渲染保持默认转义,对需要展示富文本的字段单独做白名单过滤。
- 业务层:把权限模型收紧到“项目级 + 角色级”,限制普通标注者能触达的数据范围;对管理员操作增加二次校验;给所有用户加上登录限速和会话保护。
- 基础设施层:数据库账号最小权限、Web 应用防火墙拦截常见攻击特征、Nginx 层配置安全响应头与请求体大小限制、定期审计日志。
这个逻辑对应到 doccano 上,还要考虑它本身的项目结构。doccano 是前后端分离的架构,后端是 Django REST Framework,前端是 Vue.js。前端通过 API 与后端交互,这意味着 XSS 面更大、API 接口更多,安全配置不能只看 Django 模板,还要覆盖 DRF 的认证机制、API 的访问控制,以及前端渲染时的转义行为。后面几个章节我会把每一层展开讲,并且给出可以直接用的配置片段。
2. SQL 注入防护全配置:从 ORM 到数据库权限的多层拦截
2.1 先把查询写对:ORM 参数化的正确姿势
doccano 的核心业务离不开对标注数据的检索、过滤和统计。最常见的安全隐患来自“想当然的过滤”和“图省事的原生 SQL”。我在做安全审计时,会先扫描整个项目里所有的objects.filter()、.exclude()、.annotate()以及所有包含raw()、extra()、connection.cursor()的地方。
先看一个反面教材,这种代码在一些二次开发过的 doccano 分支里真实存在:
from django.db import connection def search_projects(keyword): keyword = keyword.replace("'", "") sql = f"SELECT * FROM api_project WHERE name LIKE '%{keyword}%'" cursor = connection.cursor() cursor.execute(sql) return cursor.fetchall()我见过有人用replace()去过滤单引号,以为这样就能防注入。实际上攻击者只需要用%和_做通配符攻击,或者利用注释符--打断后续语句,甚至直接传入完整的 UNION 查询。一次替换根本挡不住。
正确的姿势是彻底放弃这种拼接。Django ORM 自带参数化机制,占位符由框架帮你处理,你不需要手动转义:
from django.db.models import Q def search_projects(keyword): return Project.objects.filter(Q(name__icontains=keyword) | Q(description__icontains=keyword))如果你确实需要执行原生 SQL,也必须用参数化游标:
from django.db import connection def search_projects(keyword): sql = """ SELECT * FROM api_project WHERE name LIKE %s OR description LIKE %s """ like_pattern = f"%{keyword}%" with connection.cursor() as cursor: cursor.execute(sql, [like_pattern, like_pattern]) return cursor.fetchall()这里的关键点是%s占位符。无论用户输入什么,数据库驱动都会把它当作纯数据,而不是可执行的 SQL 代码。这是所有注入防护的地基,地基不牢,后面配置再多都是白搭。
2.2 动态查询、列表排序与统计接口的防护方案
doccano 的列表页通常支持按名称排序、按标签过滤、按标注状态筛选,这些动态条件如果处理不当,很容易把可控参数带进 SQL。我在实际审计中发现过这样一个模式:前端传?ordering=created_at,后端拿到后就拼进order_by():
ordering = request.GET.get("ordering", "-created_at") projects = Project.objects.all().order_by(ordering)如果ordering参数来自前端且没有校验,攻击者传个id;DROP TABLE api_project--虽然不一定能直接执行,但配合异常报错能探测数据库结构;如果后端在排序前做了字符串拼接,则会产生真实的注入点。防护方法是建一个白名单映射:
ORDERING_WHITELIST = { "created_at": "created_at", "-created_at": "-created_at", "name": "name", "-name": "-name", "updated_at": "updated_at", } ordering = ORDERING_WHITELIST.get(request.GET.get("ordering", "-created_at"), "-created_at") projects = Project.objects.all().order_by(ordering)同样的思路也适用于 filter 字段。doccano 里有很多查询参数是可选的,比如按project_type过滤、按annotation_status过滤,我建议统一走“参数名 → 模型字段”的映射函数,不存在的键直接忽略,而不是把前端传来的字符串直接当字段名用。
统计接口更要小心。标注平台经常要算“每个项目标注了多少条”“每个标注者完成了多少任务”,这类聚合查询如果用extra(select={...})或RawSQL拼接动态内容,风险极高。我一般建议把聚合逻辑完全固定下来:
from django.db.models import Count, Q stats = Project.objects.annotate( total_documents=Count("documents", distinct=True), completed_annotations=Count( "documents__annotations", filter=Q(documents__annotations__is_correct=True), distinct=True ), )这种写法不涉及任何用户可控标识符,Django 会把整条 SQL 的参数化处理交给底层驱动,安全性和可读性都好得多。
2.3 数据库账号最小权限配置
即便应用层做了万无一失的参数化,也不能把数据库最高权限交给 Web 应用。这个教训我是在一次事故里学到的:某团队给 doccano 配的数据库账号直接用了 PostgreSQL 的超级用户,结果一次注入攻击把整个库的所有表都删了。后来我专门总结了一套最小权限策略:
| 项 | 建议配置 | 原因 |
|---|---|---|
| 连接账号 | 专用账号,不共享 | 便于审计和责任追踪 |
| 默认权限 | 仅 SELECT / INSERT / UPDATE / DELETE | doccano 运行只需要这些基础操作 |
| DDL 权限 | 禁止 CREATE / DROP / ALTER | 防止攻击者通过注入创建恶意对象 |
| 超管权限 | 仅运维使用,应用账号永不授权 | 即使被注入也无法扩展攻击面 |
在 PostgreSQL 里,可以通过如下方式创建最小权限账号:
CREATE USER doccano_app WITH PASSWORD '随机强密码'; GRANT CONNECT ON DATABASE doccano TO doccano_app; GRANT USAGE ON SCHEMA public TO doccano_app; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO doccano_app; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO doccano_app; REVOKE CREATE ON SCHEMA public FROM doccano_app;还有一个容易忽略的点:如果新表是在应用账号创建之后新增的,ALTER DEFAULT PRIVILEGES能解决未来表自动授权的问题。记住每次迁移数据库后检查一遍权限,别让迁移脚本把权限覆盖掉了。
2.4 敏感字段加密与脱敏
标注平台的数据价值高,但合规要求也高。doccano 默认把所有数据明文存库,一旦数据库泄露,所有标注内容、用户名、邮箱全部暴露。我的建议是至少做到两层:密码字段自动哈希(Django 默认已处理)和敏感业务字段的加密存储。
如果你在 doccano 上面接入了自定义的用户字段或私有备注内容,Django 侧可以用Fernet或django-cryptography这类库做字段级加密:
from django_cryptography.fields import encrypt class PrivateNote(models.Model): content = encrypt(models.TextField()) created_by = models.ForeignKey(User, on_delete=models.CASCADE)这种方案能保证即使数据库文件被拖走,没有密钥的人也无法还原内容。密钥管理建议放到环境变量或独立的密钥服务里,不要写进代码仓库,更不能出现在 Docker Compose 的明文配置里。
2.5 用中间件与 WAF 做兜底
应用层代码写得再安全,也挡不住未来某个版本迭代引入新漏洞。我习惯在 Nginx 层加一道正则过滤,把明显的注入特征拦截在最外层:
location /api/ { if ($query_string ~* "(\bselect\b.*\bfrom\b|union\s+select|information_schema|benchmark\s*\()") { return 403; } proxy_pass http://doccano_backend; }注意这种基于正则的过滤不能作为唯一防线,因为攻击者可以用编码绕过、注释拆分等手段绕过规则,但作为一层噪音拦截还是有效的,能显著减少扫描器的骚扰。
商用 WAF 或云平台的 Web 防火墙也是可用选项,我实测下来,对常见扫描流量的拦截率在 90% 以上。关键是配置好“严格模式”,同时开启日志记录,定期回看拦截记录,从中发现真实攻击手法。
3. XSS 防护全配置:从模板转义到 CSP 的策略组合
3.1 标注内容为什么是 XSS 重灾区
doccano 的工作流决定了它的 XSS 面很特殊:标注者可以往文本里插入任意内容,包括 URL、代码片段、HTML 标签、Emoji;审核员和项目管理员会查看这些内容并给出标注意见;平台开发者还会在管理后台里查看标注统计和导出内容。任何一个环节缺失转义,恶意脚本就能在别人的浏览器里跑起来。
更麻烦的是,doccano 的前端是 Vue.js 项目,默认的模板语法({{ }})确实会自动转义,但如果开发者为了让某段内容“看起来更好看”,用了v-html指令渲染用户提交的文本,转义就完全被跳过了。这种问题在自定义标注视图、预览面板、导出预览这类功能里很常见。
所以 XSS 防护要两头抓:后端存数据时不改原文(保留原始标注数据),渲染时按需转义或过滤;前端原则上禁止用v-html渲染任何用户可控内容,确实需要展示富文本时,先用白名单过滤库清洗一遍。
3.2 Django 模板自动转义的正确使用
Django 模板引擎默认开启自动转义,这是个好消息。但很多人在二次开发 doccano 时,会在模板里写|safe或者autoescape off,这才是翻车的开始。
自查一下你的模板里是否有这种写法:
<div> {{ annotation.text | safe }} </div>这条代码等于告诉 Django:“我不需要转义,这段内容直接输出 HTML 吧。”如果文本来自用户输入,XSS 就在这里落地了。正确做法是去掉|safe,让默认转义生效:
<div> {{ annotation.text }} </div>这样<script>alert(1)</script>会显示成纯文本,而不是被浏览器执行。
如果确实需要展示部分 HTML,比如标注工具要高亮某些关键词,我建议在后端用bleach库做白名单清洗,把允许的标签和属性明确列出来:
import bleach ALLOWED_TAGS = ["p", "br", "strong", "em", "a", "span", "mark", "b", "i"] ALLOWED_ATTRIBUTES = { "a": ["href", "title", "target"], "span": ["class", "data-*"], } ALLOWED_STYLES = ["color", "font-weight", "background-color"] cleaned_content = bleach.clean( raw_content, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES, styles=ALLOWED_STYLES, strip=True, )把这个清洗逻辑做成一个统一函数,在写入存储前和渲染输出前各执行一次,双保险。
3.3 CSP 策略配置实战
CSP(内容安全策略)是我在 doccano 防护里强烈建议加的一层,它能有效缓解即使转义失败也造成的危害。CSP 可以理解为告诉浏览器:哪些来源的脚本、样式、图片可以加载,其他的一律拦截。
在 Django 里加 CSP 响应头有两种方式:一个是手动在 Nginx 中配置,另一个是用django-csp中间件。我推荐用中间件方案,因为可以在不同视图里差异化设置。
安装并配置:
pip install django-csp# settings.py MIDDLEWARE = [ # 放在最前 "csp.middleware.CSPMiddleware", ... ] CONTENT_SECURITY_POLICY = { "DIRECTIVES": { "default-src": ["'self'"], "script-src": ["'self'", "'unsafe-inline'"], "style-src": ["'self'", "'unsafe-inline'"], "img-src": ["'self'", "data:"], "connect-src": ["'self'"], "object-src": ["'none'"], "base-uri": ["'self'"], "frame-ancestors": ["'none'"], }, }配置里有几个细节需要解释。'unsafe-inline'是为了兼容 doccano 前端里已有的内联脚本和样式,建议在完全审计后移除或收紧;frame-ancestors: 'none'能阻止页面被 iframe 嵌入,直接减少点击劫持攻击;object-src: 'none'则封堵了 Flash 等插件加载。
上线后我会用浏览器开发者工具看有没有被 CSP 拦截的资源,把确实可靠的域名加进白名单,其他的一律拦截。
3.4 上传文件的类型校验
doccano 不仅接收文本,还支持上传导入数据集文件。如果上传的是带有恶意脚本的 HTML 文件或 SVG 文件,当其他用户下载预览时,浏览器也会执行其中脚本。这类攻击属于Stored XSS的一种变体。
我的处理方案是:文件上传后先校验扩展名和 MIME 类型,然后强制重命名并改变存储路径:
ALLOWED_EXTENSIONS = {"txt", "csv", "json", "jsonl", "tsv"} ALLOWED_MIME_TYPES = { "text/plain", "text/csv", "application/json", "application/octet-stream", } def validate_upload(file_obj): ext = file_obj.name.split(".")[-1].lower() mime = file_obj.content_type if ext not in ALLOWED_EXTENSIONS or mime not in ALLOWED_MIME_TYPES: raise ValidationError("文件类型不允许")另外,上传的标注文件里可能包含 HTML 片段或链接,在导出下载时也必须进行同样的转义处理。doccano 的导出功能如果直接拼接 CSV,同样是 XSS 传播的通道。
4. 认证授权与访问控制加固
4.1 密码策略与登录限速
doccano 默认使用 Django 的用户认证体系,密码校验强度取决于AUTH_PASSWORD_VALIDATORS配置。默认配置只限制了最小长度,我一般会改成带复杂度要求的组合:
AUTH_PASSWORD_VALIDATORS = [ {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", "OPTIONS": {"min_length": 12}}, {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, ]密码要求只是第一步,真正要防的是暴力破解。我给 doccano 加了一个登录限速中间件,对同一个 IP 的登录失败次数做限制:
from django.core.cache import cache MAX_FAILED_ATTEMPTS = 5 LOCKOUT_SECONDS = 300 def check_login_lockout(ip): key = f"login_fail_{ip}" failed = cache.get(key, 0) if failed >= MAX_FAILED_ATTEMPTS: raise PermissionDenied("尝试次数过多,请稍后再试")同时在 Nginx 层限制单 IP 的请求速率,双管齐下:
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m; location /v1/auth/login/ { limit_req zone=login_limit burst=10 nodelay; proxy_pass http://doccano_backend; }4.2 双重验证与关键操作二次确认
对于管理员账号,我强烈建议开启双重验证。Django 生态里可以使用django-otp或django-two-factor-auth。虽然 doccano 本身没内置,但通过自定义认证后端可以很平滑地接入:
# settings.py AUTHENTICATION_BACKENDS = [ "django.contrib.auth.backends.ModelBackend", "django_otp.backends.OTPBackend", ]启用后,管理员登录时除了用户名密码,还必须输入一次性验证码。这个机制能挡住绝大多数因密码泄露导致的越权访问。
另外,所有“删除项目”“清空标注数据”“修改用户角色”这类关键操作,我都加了二次确认弹窗,并在后端接口上要求传入confirm=true参数,防止用户误操作或 CSRF 攻击诱导操作。
4.3 角色与项目级权限梳理
doccano 的权限模型比较扁平:管理员、项目管理员、标注员、审核员。默认配置下,管理员能操作所有项目和数据。如果团队规模大,建议按“项目隔离”重新分配权限,让标注员只能看到分配给自己的任务。
这里的核心做法是用 DRF 的DjangoObjectPermissions配合guardian库做行级权限控制:
REST_FRAMEWORK = { "DEFAULT_PERMISSION_CLASSES": [ "rest_framework.permissions.DjangoObjectPermissions", ], }并按角色设置 API 的白名单:
| 角色 | 可访问接口 | 说明 |
|---|---|---|
| 标注员 | 获取文档、提交标注、查看个人任务 | 禁止访问项目配置 |
| 审核员 | 标注员全部权限 + 查看统计报表 | 可以查看但无法修改 |
| 项目管理员 | 项目内全部权限 | 限制在项目范围内 |
| 平台管理员 | 全部 API | 仅限平台配置与用户管理 |
权限收得越细,攻击者横向移动的空间就越小。即使一个标注员账号被攻破,最多影响他有权访问的那部分数据,其他项目的数据他碰不到。
5. 完整安全配置模板:一次可落地的部署参考
5.1 前置准备
开始配置前,先把 doccano 的 Docker Compose 部署方式和 Django 配置文件摸清楚。我的经验是:在容器部署模式下,把安全配置集中放在环境变量和独立配置文件中,避免直接改镜像内的代码文件,这样可以提高可维护性。
需要准备的内容:
- 一份
settings.py的追加配置片段 - 一份 Nginx 安全配置
- 一份 PostgreSQL 账号权限脚本
- 一份部署后的自检清单
5.2 核心配置示例
下面是一份我实际用过的settings_secure.py追加配置,你可以直接复制到 doccano 项目的settings.py末尾:
# 安全增强配置 SECURE_CONTENT_TYPE_NOSNIFF = True SECURE_BROWSER_XSS_FILTER = True X_FRAME_OPTIONS = "DENY" CSRF_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True SESSION_COOKIE_HTTPONLY = True SECURE_SSL_REDIRECT = True # 限制请求体大小,防止超大请求冲击后端 DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5MB FILE_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5MB # DRF 认证与节流 REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": [ "rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.TokenAuthentication", ], "DEFAULT_THROTTLE_CLASSES": [ "rest_framework.throttling.AnonRateThrottle", "rest_framework.throttling.UserRateThrottle", ], "DEFAULT_THROTTLE_RATES": { "anon": "20/min", "user": "120/min", }, }对应 Nginx 层配置:
server { listen 443 ssl http2; server_name your_domain; ssl_certificate /etc/nginx/certs/server.crt; ssl_certificate_key /etc/nginx/certs/server.key; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Content-Security-Policy "default-src 'self'; object-src 'none'; frame-ancestors 'none';" always; client_max_body_size 10m; location / { proxy_pass http://doccano_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }5.3 部署后的自检清单
配置完成后不能直接宣布“安全了”,我每次都会跑一遍自检清单:
- 用扫描器对公网地址做一次轻量扫描,确认没有检测到 SQL 注入或 XSS 特征。
- 在本地环境模拟注入 payload:
' OR '1'='1、union select等,确认应用只返回空结果或 400,而不是数据库报错。 - 在标注内容里插入
<script>alert(1)</script>,用审核员账号打开,确认脚本被转义为普通文本。 - 检查响应头是否包含
X-Content-Type-Options: nosniff、X-Frame-Options: DENY等安全头。 - 确认数据库连接账号不是超级用户,且没有 DDL 权限。
- 确认登录接口连续失败 5 次后出现限流提示。
- 检查日志里是否有来自异常 IP 的扫描行为,有则通知运维封禁。
这套清单我也整理成了表格,方便每次上线或更新版本后逐项勾选:
| 检查项 | 命令/方式 | 预期结果 |
|---|---|---|
| SQL 注入防护 | 使用注入 payload 测试列表接口 | 返回空结果或 400,无 SQL 报错 |
| XSS 防护 | 插入脚本标签后预览 | 显示为纯文本 |
| 安全响应头 | curl -I查看响应头 | 包含 nosniff、X-Frame-Options 等 |
| 数据库最小权限 | 登录数据库执行CREATE TABLE | 权限不足报错 |
| 登录限速 | 连续错误登录 6 次 | 出现锁定提示 |
| 日志审计 | 查看 nginx/application 日志 | 无异常扫描记录 |
6. 常见问题排查与实战避坑
6.1 高频问题速查表
| 问题 | 原因分析 | 解决方案 |
|---|---|---|
| 页面能打开但 API 接口报 403 | 安全头或 CSP 拦截了资源请求 | 检查connect-src和frame-ancestors配置,放行合法资源 |
| 管理员登录后被强制退出 | SESSION_COOKIE_SECURE开启但未配置 HTTPS | 先完成 HTTPS 配置再开启此选项 |
| 标注文本里的 URL 无法正常点击 | 模板默认转义导致链接没有href属性 | 采用bleach白名单清洗后渲染,而不是盲加safe |
| 导入 CSV 时部分行被过滤 | DATA_UPLOAD_MAX_MEMORY_SIZE限制了请求体大小 | 按需调高,但不要超过 Nginx 的client_max_body_size |
| 用户反馈登录变慢 | 登录限速策略误伤正常玩家 | 调整限速阈值并设置放行白名单 IP |
| 数据库迁移失败 | 最小权限账号缺少 DDL 权限 | 迁移时用运维账号,迁移完切回应用账号 |
6.2 踩坑实录
这里记录一个让我印象深刻的坑:有一次我给 doccano 加了 CSP,结果前端图表全部变成空白,调试了很久才发现是img-src里漏掉了data:协议——图表库内部用 base64 生成了图片数据。这个案例说明,加安全头不能闭眼抄配置,必须结合项目实际运行情况做微调。
另一个坑是SECURE_SSL_REDIRECT。在容器内部署时,如果前端 Nginx 已经把 HTTP 转成了 HTTPS,后端实际上接收的是内部 HTTP 请求,再开SECURE_SSL_REDIRECT会导致无限重定向。解决办法是只在 Nginx 层做跳转,后端不开启这个选项,或者使用X-Forwarded-Proto头判断。
6.3 安全意识永远比配置本身重要
安全配置不是一次性工作。doccano 隔几个月就更新一次,每次更新都要重新检查一遍依赖版本,确认有没有新的 CVE 曝光。我个人的习惯是订阅相关的安全通告,每次发布新版本优先在测试环境跑一遍自检清单,再考虑是否升级生产环境。
如果团队没有专职安全人员,至少做到两条:一是永远不要用默认密码,二是永远不要把数据库端口直接暴露到公网。这两条能挡住一大半的低级攻击。
说实话,给 doccano 做安全加固这件事,投入产出比非常高。标注数据本身就是团队的核心资产,与其等出了事再补救,不如在部署初期就把这些配置写清楚。这篇指南里的每一行配置我都实际跑过,踩过的坑也写出来了,照着做不能说绝对安全,但至少能把攻击者的路堵掉九成。剩下的,交给持续关注和定期复查。