PG8000或Postgres协议是否不支持DDL语句中的绑定参数?
我尝试用PG8000创建PostgreSQL新用户,直接写死密码的代码可以正常运行,但密码会被写入服务器日志,存在安全隐患(本场景暂不考虑SQL注入):
with pg8000.dbapi.connect(**CONNECTION_INFO) as conn: csr = conn.cursor() csr.execute("create user example password 'password-123'") conn.commit()
之后用绑定参数查询用户信息是正常的:
with pg8000.dbapi.connect(**CONNECTION_INFO) as conn: csr = conn.cursor() csr.execute("select * from pg_user where usename = %s", ("example",)) result = csr.fetchall()
但尝试用绑定参数创建用户时请求失败:
with pg8000.dbapi.connect(**CONNECTION_INFO) as conn: csr = conn.cursor() csr.execute("create user example password %s", ("password-123",)) conn.commit()
客户端报错:
DatabaseError: {'S': 'ERROR', 'V': 'ERROR', 'C': '42601', 'M': 'syntax error at or near "$1"', 'P': '30', 'F': 'scan.l', 'L': '1145', 'R': 'scanner_yyerror'}
服务器端日志报错:
2022-08-26 12:30:02.029 UTC [205] LOG: statement: begin transaction 2022-08-26 12:30:02.029 UTC [205] ERROR: syntax error at or near "$1" at character 30 2022-08-26 12:30:02.029 UTC [205] STATEMENT: create user example password $1
使用PG8000原生接口也会出现同样问题。
换成psycopg2后,带参数执行创建命令可以成功,但服务器日志显示客户端已经完成了参数替换,发送的是包含明文密码的完整SQL语句:
2022-08-26 12:30:55.317 UTC [206] LOG: statement: BEGIN 2022-08-26 12:30:55.317 UTC [206] LOG: statement: create user example2 password 'password-123' 2022-08-26 12:30:55.317 UTC [206] LOG: statement: COMMIT
请问PG8000或Postgres客户端-服务器协议是否不支持DDL语句中的绑定参数?
PostgreSQL的服务器端参数绑定(即把参数和SQL语句分开发送给服务器,由服务器完成替换)仅支持在DML语句(如SELECT、INSERT、UPDATE、DELETE)的特定位置使用,DDL语句(如CREATE USER、CREATE TABLE)的语法中,很多位置并不接受参数占位符。
你遇到的报错本质是Postgres解析create user example password $1时,认为$1的位置不符合语法规则——Postgres的DDL语法里,PASSWORD后面需要跟一个字符串字面量,而不是参数占位符,所以服务器会直接抛出语法错误,这和PG8000无关,是Postgres本身的限制。
至于psycopg2能成功执行,是因为它做了客户端侧的参数替换:在本地把占位符换成实际的密码值,再把完整的SQL语句发送给服务器,所以服务器日志里会看到明文密码,本质上和你直接写死密码的方式没有安全区别。
如果要避免密码写入服务器日志,你可以使用PostgreSQL的CREATE USER语句的另一种写法:调用crypt函数处理密码,同时使用参数绑定传递密码明文,示例如下:
with pg8000.dbapi.connect(**CONNECTION_INFO) as conn: csr = conn.cursor() csr.execute("create user example password crypt(%s, gen_salt('bf'))", ("password-123",)) conn.commit()
这种写法中,crypt函数的参数可以使用占位符,服务器会正确解析,同时日志里只会记录crypt($1, gen_salt('bf')),不会出现明文密码。
内容的提问来源于stack exchange,提问作者kdgregory

