问题
I'm trying to make a PHP interface to my PSQL database, and I would like a few local users registered on PSQL to log into my DB. I would first create each username for each user, with a generic password like 'Password123', and then the user could latter change his/her password.
To do that, I thought of using a simple PHP form:
<form action="" method="post">
<table>
<tr> <td> User: </td> <td> <input type="text" name="user" /> </td> </tr>
<tr> <td> Old password: </td> <td> <input type="password" name="old" /> </td> </tr>
<tr> <td> New password: </td> <td> <input type="password" name="new1" /> </td> </tr>
<tr> <td> Repeat new password: </td> <td> <input type="password" name="new2" /> </td> </tr>
<tr> <td> <input type="submit" name="submit" value="Change password" /> </td> </tr>
</form>
<?php
if ($_POST) {
$user = $_POST["user"];
$old = $_POST["old"];
$new1 = $_POST["new1"];
$new2 = $_POST["new2"];
$link = pg_connect("dbname=mydb host=localhost user=$user password=$old connect_timeout=1");
if (!$link) {
die("Error connecting to mydb: ".pg_last_error($link));
}
if ($new1 <> $new2) {
pg_close($link);
die("New passwords do not match.");
}
$res = @pg_query($link,"ALTER ROLE $user WITH ENCRYPTED PASSWORD '$new1';");
if ($res) {
echo "Password successfully changed!<br>";
} else {
echo "Failed to change password...<br>";
}
pg_close($link);
}
?>
It does work just as I expected!
But I read that there are some SQL Injection Attacks that could be made on expressions like this where there is a simple variable interpolation inside the SQL query. And so I read that PREPARE
statements are the safest way to do such queries. I expected to do something like:
pg_prepare($link,"change_user","ALTER ROLE $1 WITH ENCRYPTED PASSWORD '$2';");
But I get a syntax error, even if I try to PREPARE
this command in pgAdminIII. Indeed, Postgres manual indicates that PREPARE
can only prepare "Any SELECT, INSERT, UPDATE, DELETE, or VALUES statement."
Then I tried to use the pg_escape_string()
function:
$user = pg_escape_string($_POST["user"]);
...
This works well when I use normal passwords, and if I try to set a password like ''"
, I cannot change it afterwards. I have tried pg_escape_literal
which is preferred to pg_escape_string
according to the manual, but my PHP is version 5.3.13, and this command is only for 5.4.4 onwards.
The question is: is this the best way to prevent injection attacks in this case? Does this really prevent attacks?
Another question: if a user wants to use an apostrophe in his/her password, is it possible without this error?
回答1:
As a note, I would recommend using a stored procedure for this. A prepared statement if needed could always call the stored procedure.
Here's some very simple sample code, largely copied from LedgerSMB (and edited a bit):
CREATE OR REPLACE FUNCTION save_user(
in_username text,
in_password TEXT
) returns bool
SET datestyle = 'ISO, YMD' -- needed due to legacy code regarding datestyles
AS $$
DECLARE
stmt text;
t_is_role bool;
BEGIN
-- WARNING TO PROGRAMMERS: This function runs as the definer and runs
-- utility statements via EXECUTE.
-- PLEASE BE VERY CAREFUL ABOUT SQL-INJECTION INSIDE THIS FUNCTION.
PERFORM rolname FROM pg_roles WHERE rolname = in_username;
t_is_role := found;
IF t_is_role is true and t_is_user is false and in_pls_import is false THEN
RAISE EXCEPTION 'Duplicate user';
END IF;
if t_is_role and in_password is not null then
execute 'ALTER USER ' || quote_ident( in_username ) ||
' WITH ENCRYPTED PASSWORD ' || quote_literal (in_password)
|| $e$ valid until $e$ ||
quote_literal(now() + '1 day'::interval);
elsif t_is_role is false THEN
-- create an actual user
execute 'CREATE USER ' || quote_ident( in_username ) ||
' WITH ENCRYPTED PASSWORD ' || quote_literal (in_password)
|| $e$ valid until $e$ || quote_literal(now() + '1 day'::interval);
END IF;
return true;
END;
$$ language 'plpgsql' SECURITY DEFINER;
Note that this is a security definer function. It is generally recommended that you set this function to be owned by a user which is not a database superuser. Such a user would need createrole privileges and access to whatever tables you want to manage here. Also note the warning heavily. In-db SQL-injection is possible if you don't appropriately escape your parameters. Note that this function can both create a user and change a password. The LSMB-specific portions regarding detecting whether the user is set up and handling the case if not are omitted here.
回答2:
As you found out, prepared statements cannot be used for "utility statements" like ALTER USER
, they're out of the scope of this question.
The user and the password must be properly quoted, and to do it properly there are several issues to consider.
The user is an identifier and the password is a string literal, which is why the identifier is not surrounded by single quotes whereas the password is. They do not follow the same syntactic rules and can't be quoted with same functions.
php-5.4.4 provides pg_escape_identifier for identifiers but older versions provide nothing to quote identifiers. pg_escape_string
is not suitable for that, as noted in its description and user comments. PostgreSQL itself provides the quote_ident
function, but in order to call it, a separate query should be made. Basically this could look like this:
$pgr=pg_query_params($dbconn, 'SELECT quote_ident($1)',
array(pg_escape_string($_POST["user"])));
// error check on $pgr omitted for brievity
list($quoted_user) = pg_fetch_array($pgr);
$quoted_password = pg_escape_string($_POST["password"]);
$res=pg_query($dbconn,
"ALTER USER $quoted_user WITH ENCRYPTED PASSWORD '$quoted_password'");
However, you may question whether it's a good idea to let users choose any password they want, without filtering.
Among the problems it causes, one that raises immediately is in your connect call:
$link = pg_connect("dbname=mydb host=localhost user=$user password=$old connect_timeout=1");
Here the password in $old
is not quoted and if it contains, say a space character, this is going to fail. This is the likely reason of this problem you mention: if I try to set a password like ''", I cannot change it afterwards.
To handle special characters in the pg_connect
call, the doc says this:
Each parameter setting is in the form keyword = value. Spaces around the equal sign are optional. To write an empty value or a value containing spaces, surround it with single quotes, e.g., keyword = 'a value'. Single quotes and backslashes within the value must be escaped with a backslash, i.e., \' and \.
So if you're set on accepting anything, you should provide a function that does this escaping.
Personally, I would forbid these characters in the first place in user-supplied passwords on the grounds that it's a needless source of problems. As well as non US-ASCII characters because of encoding issues.
来源:https://stackoverflow.com/questions/18897231/how-to-parameterize-an-alter-role-statement-in-postgresql