postgresql_user: add trust_input parameter (#116)

* postgresql: add input checks for potentially dangerous substrings

* postgresql_user: add trust_input parameter

* add CI, add changelog fragment

* fix CI

* moved input patterns outside is_input_dangerous function

* Update plugins/module_utils/database.py

Co-Authored-By: Thomas O'Donnell <andytom@users.noreply.github.com>

* Update plugins/module_utils/database.py

Co-Authored-By: Thomas O'Donnell <andytom@users.noreply.github.com>

* fix

Co-authored-by: Thomas O'Donnell <andytom@users.noreply.github.com>
This commit is contained in:
Andrew Klychkov 2020-04-12 14:16:44 +03:00 committed by GitHub
commit 6d7f66539c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 142 additions and 1 deletions

View file

@ -26,6 +26,21 @@
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import re
# Input patterns for is_input_dangerous function:
#
# 1. '"' in string and '--' in string or
# "'" in string and '--' in string
PATTERN_1 = re.compile(r'(\'|\").*--')
# 2. union \ intersect \ except + select
PATTERN_2 = re.compile(r'(UNION|INTERSECT|EXCEPT).*SELECT', re.IGNORECASE)
# 3. ';' and any KEY_WORDS
PATTERN_3 = re.compile(r';.*(SELECT|UPDATE|INSERT|DELETE|DROP|TRUNCATE|ALTER)', re.IGNORECASE)
class SQLParseError(Exception):
pass
@ -140,3 +155,47 @@ def mysql_quote_identifier(identifier, id_type):
special_cased_fragments.append(fragment)
return '.'.join(special_cased_fragments)
def is_input_dangerous(string):
"""Check if the passed string is potentially dangerous.
Can be used to prevent SQL injections.
Note: use this function only when you can't use
psycopg2's cursor.execute method parametrized
(typically with DDL queries).
"""
if not string:
return False
for pattern in (PATTERN_1, PATTERN_2, PATTERN_3):
if re.search(pattern, string):
return True
return False
def check_input(module, *args):
"""Wrapper for is_input_dangerous function."""
needs_to_check = args
dangerous_elements = []
for elem in needs_to_check:
if isinstance(elem, str):
if is_input_dangerous(elem):
dangerous_elements.append(elem)
elif isinstance(elem, list):
for e in elem:
if is_input_dangerous(e):
dangerous_elements.append(e)
else:
elem = str(elem)
if is_input_dangerous(elem):
dangerous_elements.append(elem)
if dangerous_elements:
module.fail_json(msg="Passed input '%s' is "
"potentially dangerous" % ', '.join(dangerous_elements))

View file

@ -147,6 +147,11 @@ options:
description:
- Add a comment on the user (equal to the COMMENT ON ROLE statement result).
type: str
trust_input:
description:
- If C(no), check whether values of some parameters are potentially dangerous.
type: bool
default: yes
notes:
- The module creates a user (role) with login privilege by default.
Use NOLOGIN role_attr_flags to change this behaviour.
@ -252,7 +257,11 @@ except ImportError:
pass
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.database import pg_quote_identifier, SQLParseError
from ansible_collections.community.general.plugins.module_utils.database import (
pg_quote_identifier,
SQLParseError,
check_input,
)
from ansible_collections.community.general.plugins.module_utils.postgres import (
connect_to_db,
get_conn_params,
@ -812,6 +821,7 @@ def main():
session_role=dict(type='str'),
groups=dict(type='list', elements='str'),
comment=dict(type='str', default=None),
trust_input=dict(type='bool', default=True),
)
module = AnsibleModule(
argument_spec=argument_spec,
@ -838,6 +848,12 @@ def main():
groups = [e.strip() for e in groups]
comment = module.params["comment"]
trust_input = module.params['trust_input']
if not trust_input:
# Check input for potentially dangerous elements:
check_input(module, user, password, privs, expires,
role_attr_flags, groups, comment)
conn_params = get_conn_params(module, module.params, warn_db_default=False)
db_connection = connect_to_db(module, conn_params)
cursor = db_connection.cursor(cursor_factory=DictCursor)