聊天记录搜索如何安全高亮?把消息当文本,把 mark 当结构

简介: 本文以米米商聊为背景,探讨聊天记录文字搜索的高亮安全实现。强调“数据进文本节点、结构由程序控制”,通过`splitLiteralHighlights`与`renderLiteralHighlights`分离匹配与渲染,确保原文零篡改、DOM零注入。代码为教学示例,不反映产品实际逻辑。(239字)

聊天记录搜索找到了一条消息,接下来还要把命中的关键词标出来。常见写法是在消息字符串里插入 <mark>,再赋给 innerHTML。它看起来只增加了一层黄色背景,却同时改变了消息的解释方式:原本只是文字的内容,进入了 HTML 解析器。

以米米商聊为背景,已整理的手机端资料支持按文字关键词搜索聊天记录,并定位上下文;资料不支持把图片或视频内容说成可按关键词搜索。本文只借文字搜索讨论结果展示,不断言产品提供了下文的高亮方式。代码、规则与数据均为独立教学示例,不代表产品源码或已发现的产品问题。

1. 命中关键词,不代表消息可以作为 HTML

假设合成消息是 讨论 <img src=x onerror=alert(1)> 这段标签,关键词是 img。搜索命中之后,读者期待看到原文中的标签文字,而不是生成一张图片。

直接拼接标签再交给 innerHTML,可能改变网页结构,某些输入还会触发事件处理器。只考虑 <script> 是否执行并不足够。MDN 的 innerHTML 文档说明了 HTML 解析与注入风险。

这里需要分清两件事:消息和关键词都是待展示的数据;mark 元素是程序允许创建的结构。让数据始终进入文本节点,让程序决定节点类型。 不让消息决定标签名、属性或事件处理器。

文字搜索高亮的文本片段与固定节点流程,以及必须保持的原文不变量

2. 先拆成片段,不返回一段 HTML

示例采用明确的匹配规则:区分大小写、按完整关键词字面匹配,从左到右选择不重叠的命中。关键词为空时不高亮;限制高亮数量后,剩余内容仍完整展示为普通文本。

原文与关键词 预期结果
资料已收到,资料稍后归档 / 资料 两处命中,原文不变
a.*b 与 a.*b / a.*b 点号和星号就是普通字符
aaaa / aa 两处相邻命中,不重复覆盖字符
aaa / aa 第一处命中,最后一个 a 保留为普通文本
任意原文 / 空字符串 完整原文,无高亮

片段的数据结构只有 text 与 hit。最重要的不变量是:按顺序连接所有片段的 text,结果必须与输入原文完全相同。高亮不能悄悄删除、替换或重复字符。

function splitLiteralHighlights(text, keyword, limit = 100) {
   
  if (typeof text !== "string" || typeof keyword !== "string") {
   
    throw new TypeError("text and keyword must be strings");
  }
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) {
   
    throw new RangeError("limit must be an integer from 1 to 200");
  }
  if (keyword === "") return [{
    text, hit: false }];
  const parts = [];
  let cursor = 0;
  let count = 0;
  while (count < limit) {
   
    const start = text.indexOf(keyword, cursor);
    if (start === -1) break;
    if (start > cursor) {
   
      parts.push({
    text: text.slice(cursor, start), hit: false });
    }
    parts.push({
    text: text.slice(start, start + keyword.length), hit: true });
    cursor = start + keyword.length;
    count += 1;
  }
  if (cursor < text.length) {
   
    parts.push({
    text: text.slice(cursor), hit: false });
  }
  return parts.length ? parts : [{
    text, hit: false }];
}

function renderLiteralHighlights(container, text, keyword, limit = 100) {
   
  const parts = splitLiteralHighlights(text, keyword, limit);
  const doc = container.ownerDocument;
  const fragment = doc.createDocumentFragment();
  for (const part of parts) {
   
    const node = part.hit
      ? doc.createElement("mark")
      : doc.createTextNode(part.text);
    if (part.hit) node.textContent = part.text;
    fragment.append(node);
  }
  container.replaceChildren(fragment);
  return parts.filter(part => part.hit).length;
}

数量上限 100、允许配置的范围 1~200 是教学设计,不是产品限制,也不等于搜索结果总数。输入校验先完成,随后才替换展示节点。异常输入不会把这个容器中的旧内容提前清空。

indexOf 与 slice 在本例中使用一致的 UTF-16 索引,因此可以保持原文。但这不承诺命中一定覆盖完整的用户可见字符:组合附加符号、表情序列与字素簇需要另行约定匹配规则。

3. 用固定的 mark 节点展示命中内容

渲染函数只创建两类节点:普通片段使用文本节点,命中片段使用程序写死的 mark 元素,并通过 textContent 放入文字。即使片段包含尖括号,也不会因为这两步而变成图片、脚本或链接。

createTextNode创建文本节点;textContent写入文本内容。这里的保护来自节点与数据入口的选择,不需要先把消息内容拼成 HTML,再尝试补救。

下面是浏览器演示。把上一段代码先加载到页面,再执行这一段;result 是专门用于消息正文的展示容器。

const result = document.createElement("p");
const source = "讨论 <img src=x onerror=alert(1)> 这段标签";
const count = renderLiteralHighlights(result, source, "img");
document.body.append(result);

console.assert(result.textContent === source);
console.assert(count === 1);
console.assert(result.querySelectorAll("mark").length === 1);
console.assert(result.querySelector("img") === null);
console.assert(result.querySelector("script") === null);
console.log(result.textContent);

预期看到一整段标签文字,其中 img 被标出;DOM 中只有一个 mark,没有消息生成的 img 或 script 元素。这个结论针对本函数的文本展示路径,不代表整个应用的所有入口都已完成安全审查。

replaceChildren 会替换容器的全部子节点,所以不要把头像、按钮、消息操作菜单一起放进这个容器。重复渲染时,函数始终从原文重新生成片段,不能读取上一次的 HTML 再套一层高亮,否则容易产生嵌套标记或错误偏移。

如果消息本来就包含可信富文本,本文的纯文本渲染器会把标签作为文字显示。需要保留格式时,应单独设计受控富文本解析、内容清理和文本节点遍历,不能把这里的输入改成 innerHTML 就宣称同样安全。

4. 高亮数量限制后,尾部原文仍要留下

数量限制解决的是创建过多高亮节点的问题。本例达到上限后,将未处理的剩余原文作为一个普通片段追加;它既不截断消息,也不假装后面已经没有命中。

例如原文 资料资料资料,关键词 资料,上限为 1。返回结果是一段命中的 资料 与普通尾部 资料资料。渲染函数返回 1,只表示实际展示了一个高亮节点;若界面需要总命中数,应使用单独的统计规则,不能拿这个返回值作为搜索总数。

这个上限也不限制原文长度或一次页面上的消息数量。大批结果还需要分页、虚拟列表或其他展示策略。本文没有做性能基准,不据此声称能处理任意规模的数据。

5. 用输入与 DOM 两层验证,而不是只看黄色背景

本例验证分别检查片段和实际节点:片段连起来是否保持原文,命中片段是否等于关键词,数量是否遵守上限;DOM 是否只产生预定的标记节点,重复渲染是否清掉旧标记,标签文字是否仍然只是文本。

验证输入 要检查的边界
中文、空字符串、换行与相邻命中 原文完整,不漏字、不重复
正则元字符、尖括号与引号 按字面匹配,不解释成模式或属性
完整表情与两种不同的重音编码 精确匹配的范围如实体现
合成图片标签、脚本标签、实体样式文字 不产生输入指定的元素或事件属性
连续两次使用不同关键词渲染 不累积旧 mark,不动容器外的控件
非字符串输入或非法数量 抛错,原展示内容保持不变

配套浏览器验证覆盖这些边界,并用固定种子的 500 组组合输入检查原文不变量。它测试的是独立示例,没有接入真实聊天记录、搜索服务或产品客户端;也不是对所有注入方式的穷尽证明。

精确匹配还有展示层面的取舍。Café 中预组合的 é 与由 e 加组合符号构成的序列并不相同。本例不会自动归一化,也不忽略大小写。若检索服务采用了归一化、分词或不区分大小写的规则,前端应拿到对应原文的可靠命中范围,否则可能出现“能搜到但标不出”的现象。

6. 接入搜索结果时,守住原文与容器边界

先核对检索规则与展示规则是否一致,再把原文和关键词交给片段函数。渲染层只接收专用正文容器,使用固定节点类型;点击结果后的上下文定位仍由消息身份与会话身份负责,高亮片段不能代替这些定位信息。

同时检查导出、预览、弹窗和移动端结果列表有没有另一条直接拼接 HTML 的路径。框架自带的文本插值通常有自己的转义机制,但绕到原始 HTML 插入接口时,需要重新评估数据入口,不能把组件名称当成保证。

评估结果时可以抓住两项:展示文本与原文是否完全一致,DOM 结构是否只来自程序允许的节点。前者保证高亮没有改写消息,后者保证消息没有取得决定网页结构的权限。

本文由 AI 辅助整理,独立示例经过本地片段验证与浏览器 DOM 验证。产品背景只使用已整理的手机端功能资料;通用工程建议不代表产品内部实现或漏洞结论。

相关文章
|
安全 Docker 容器
漏洞扫描–-Awvs&Nessus(Docker版)
漏洞扫描–-Awvs&Nessus(Docker版)
漏洞扫描–-Awvs&Nessus(Docker版)
|
21天前
|
缓存 监控 NoSQL
Redis 缓存实战:穿透、击穿、雪崩到底差在哪,以及一套能落地的预热方案
把穿透、击穿、雪崩三个高频混用的概念一次讲清:触发条件、破坏路径、处置手段逐项区分,配空值缓存、布隆过滤器、互斥锁、逻辑过期、TTL 打散等可直接照抄的代码与配置,并给出定位热点、分层预热、灰度放量、监控回看的落地流程和架构选型、容量计费要点。读完能带走一套可复用的缓存保护清单。
|
21天前
|
弹性计算 Linux Docker
2026年10月Docker镜像源清单:ECS旧入口与公共候选
本文梳理2023年10月主流容器镜像加速入口现状:阿里云、百度旧加速器已停更或终止服务;腾讯、华为云内网地址限特定环境;毫秒镜像(docker.1ms.run)与DaoCloud为可用公共候选。强调需按“环境适配→显式拉取→摘要校验”三步验证,不可盲目替换配置。(239字)
2026年10月Docker镜像源清单:ECS旧入口与公共候选
|
21天前
|
域名解析 网络协议 算法
免费SSL证书申请流程:2026年最新阿里云个人测试证书申请与部署全流程
本文详解阿里云“个人测试证书”(原免费SSL证书)申请全流程:含控制台入口、0元申领20张、域名绑定、DNS验证及CA审核,适用于个人项目与测试环境,有效期90天。阿里云SSL官网:https://t.aliyun.com/U/m0alhG
226 0
|
25天前
|
SQL 人工智能 前端开发
简单认识下Qoder Skills超市
Qoder Skills Marketplace 是面向AI Agent的通用能力市场,涵盖4.3万+ Skills(工作手册)、176个Plugins(能力工具箱)和290个Connectors(数据连接器)。聚焦“场景专业化”,覆盖研发、设计、电商、金融、法律等多元领域,突出淘宝系生态能力(如1688、Quick BI、百炼RAG),强调即插即用与真实业务集成。(239字)
264 0
|
存储 Linux 索引
CentOS7下同步阿里云CentOS7镜像并自建本地yum仓库
CentOS7下同步阿里云CentOS7镜像并自建本地yum仓库
3776 0
CentOS7下同步阿里云CentOS7镜像并自建本地yum仓库
|
11月前
|
人工智能 数据挖掘 BI
企业智能体定制的公司有哪些
助君跃专注制造业中小企业AI服务,深耕生产数据挖掘与管理提效,以务实沟通、定制化方案、灵活付费和贴心落地服务赢得口碑。相比大厂,其更贴近企业实际,助力降本增效,精准响应数字化转型需求,彰显细分领域智能化价值。(238字)
|
存储 Cloud Native API
Docker镜像管理:为什么Harbor是首选
Docker镜像管理:为什么Harbor是首选
|
存储 监控 前端开发
docker仓库--harbor
docker仓库--harbor
905 0
docker仓库--harbor
|
缓存 应用服务中间件 Linux
局域网自建YUM仓库
在公司的局域网内自建一个yum仓库,对于不能连外网的环境非常有用。同时也提高了内网服务器的下载速度。
1101 0