PostgreSQL全文搜索中的注入风险主要集中在tsquery输入处理和自定义词典配置环节。直接使用未经处理的用户输入构建tsquery查询会导致操作符注入,攻击者可能通过tsquery语法读取敏感数据或执行未授权操作。例如,用户在搜索框中输入"apple | (SELECT * FROM users)",如果后端直接拼接成tsquery,会触发SQL注入。解决方法是对所有用户输入进行严格的词素化处理和转义,使用plainto_tsquery()或phraseto_tsquery()函数而非字符串拼接。

PostgreSQL全文搜索注入的三种攻击向量

第一种是tsquery操作符注入。PostgreSQL的tsquery语法支持AND(&)、OR(|)、NOT(!)等操作符,攻击者可通过注入这些操作符改变搜索逻辑。比如输入"apple | (SELECT * FROM users)"中的竖杠会扩展查询范围。更危险的是注入FOLLOWED BY操作符,可能触发子查询执行。

第二种是词典函数注入。当使用自定义词典配置时,如果词典函数调用未经验证的用户输入,可能通过CREATE TEXT SEARCH CONFIGURATION或ALTER TEXT SEARCH CONFIGURATION语句实现权限提升。例如攻击者注入包含分号的词典参数,可能终止原语句执行后续恶意命令。

第三种是ts_headline函数XSS攻击。该函数用于高亮显示搜索结果中的匹配词,如果返回内容直接输出到网页且未转义HTML特殊字符,可能造成跨站脚本攻击。虽然这不属于SQL注入范畴,但同样是全文搜索功能常见的安全漏洞。

实际攻击场景演示

假设有一个简单的全文搜索实现:

-- 危险示例:直接拼接用户输入
SELECT * FROM articles 
WHERE to_tsvector('english', content) @@ to_tsquery('english', '''' || user_input || '''');

当user_input值为"apple') || (SELECT current_user) || ('test"时,实际执行的查询变为:

SELECT * FROM articles 
WHERE to_tsvector('english', content) @@ 
to_tsquery('english', 'apple') || (SELECT current_user) || ('test');

这将执行子查询SELECT current_user,泄露数据库当前用户信息。更严重的攻击可能通过CASE语句逐字符提取敏感数据。

五层防御策略

第一层:输入预处理。对所有用户输入进行词素化转换,只保留有效的搜索词汇。使用PostgreSQL内置的plainto_tsquery()函数自动处理特殊字符:

-- 安全用法
SELECT * FROM articles 
WHERE to_tsvector('english', content) @@ plainto_tsquery('english', user_input);

该函数会将用户输入中的操作符转义为普通词汇,比如"apple | orange"会被处理为"apple"和"orange"的AND查询。

第二层:参数化查询。即使使用全文搜索函数,也应通过参数化查询传递用户输入,避免任何形式的字符串拼接:

-- 使用参数化查询
PREPARE search_plan (text) AS
SELECT * FROM articles 
WHERE to_tsvector('english', content) @@ plainto_tsquery('english', $1);
EXECUTE search_plan(user_input);

第三层:权限最小化。创建专门用于全文搜索的数据库角色,仅授予必要的权限:

CREATE ROLE search_user;
GRANT CONNECT ON DATABASE mydb TO search_user;
GRANT SELECT ON articles TO search_user;
REVOKE EXECUTE ON FUNCTION pg_read_file FROM search_user;
REVOKE EXECUTE ON FUNCTION pg_ls_dir FROM search_user;

第四层:输出过滤。对ts_headline()函数的返回结果进行HTML编码,防止XSS攻击。在应用层对高亮文本进行转义,或使用安全的模板引擎自动转义。

第五层:词典安全配置。审核所有自定义词典函数,确保不执行动态SQL。检查text search configuration的权限设置:

-- 查看全文搜索配置权限
SELECT cfgname, nspname, cfgowner::regrole
FROM pg_ts_config 
JOIN pg_namespace ON pg_ts_config.cfgnamespace = pg_namespace.oid;
高级防护:自定义验证函数

对于需要保留部分操作符的高级搜索场景,可创建白名单验证函数:

CREATE OR REPLACE FUNCTION sanitize_tsquery(input_text text)
RETURNS text AS $$
DECLARE
    cleaned_text text;
BEGIN
    -- 移除所有非字母数字和允许的操作符
    cleaned_text := regexp_replace(input_text, '[^a-zA-Z0-9\s&|!<>():*]', '', 'g');
    
    -- 验证括号匹配
    IF (length(regexp_replace(cleaned_text, '[^()]', '', 'g')) % 2) != 0 THEN
        RAISE EXCEPTION 'Unbalanced parentheses in search query';
    END IF;
    
    -- 限制操作符连续出现
    IF cleaned_text ~ '(&|\|){2,}' THEN
        RAISE EXCEPTION 'Invalid operator sequence';
    END IF;
    
    RETURN cleaned_text;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
监控与审计方案

启用PostgreSQL的日志记录功能,监控异常的全文搜索查询:

-- 在postgresql.conf中配置
log_statement = 'all'
log_line_prefix = '%t [%p]: user=%u,db=%d,app=%a '
log_min_duration_statement = 0

-- 创建专用审计表
CREATE TABLE tsquery_audit (
    id serial PRIMARY KEY,
    search_query text,
    search_time timestamptz DEFAULT now(),
    client_ip inet,
    user_agent text
);

部署实时监控规则,检测包含敏感关键词的查询模式。例如,当tsquery中出现"pg_"、"user"、"password"等系统表名或函数名时触发警报。同时定期审计text search configuration的变更,记录所有ALTER TEXT SEARCH CONFIGURATION操作。

企业级部署建议

在生产环境中,建议采用四层架构:第一层是应用输入验证,使用正则表达式过滤非法字符;第二层是数据库代理,解析所有SQL语句并拦截可疑的tsquery结构;第三层是数据库内置防护,通过扩展如pg_sanitize实现自动转义;第四层是网络层防护,使用WAF规则检测全文搜索注入特征。

对于高安全需求场景,可考虑将全文搜索功能迁移到专用搜索服务,如使用Elasticsearch或专用搜索中间件,实现应用层与数据层的完全分离。这样即使搜索功能被注入,攻击者也无法直接访问主数据库。

版本差异与兼容性

PostgreSQL 9.6之前版本存在更多注入风险,因为tsquery语法解析较为宽松。从PostgreSQL 10开始,增加了更严格的语法检查。建议至少使用PostgreSQL 12以上版本,并启用所有安全更新。特别注意不同语言配置的差异,例如使用'simple'词典配置时,某些特殊字符处理方式与'english'配置不同。

最后需要强调的是,没有任何单一防护措施是绝对安全的。必须建立纵深防御体系,结合输入验证、参数化查询、最小权限原则和持续监控,才能有效防范PostgreSQL全文搜索中的注入风险。每次升级PostgreSQL版本或修改全文搜索配置后,都应重新进行安全测试,特别是对边界情况和异常输入的测试。