Allow editing of data where a primary key column includes a % sign in the value. Fixes #4036

Fix an XSS issue seen in View/Edit data mode if a column name includes HTML. Fixes #4367
This commit is contained in:
Aditya Toshniwal 2019-06-20 12:21:37 +01:00 committed by Dave Page
parent 5c0ea0c012
commit 6e8ebbd375
10 changed files with 112 additions and 23 deletions

View File

@ -16,6 +16,7 @@ Bug fixes
********* *********
| `Bug #3994 <https://redmine.postgresql.org/issues/3994>`_ - Fix issue where the dependencies tab for inherited tables/foreign keys shows partial text. | `Bug #3994 <https://redmine.postgresql.org/issues/3994>`_ - Fix issue where the dependencies tab for inherited tables/foreign keys shows partial text.
| `Bug #4036 <https://redmine.postgresql.org/issues/4036>`_ - Allow editing of data where a primary key column includes a % sign in the value.
| `Bug #4171 <https://redmine.postgresql.org/issues/4171>`_ - Fix issue where reverse engineered SQL was failing for foreign tables, if it had "=" in the options. | `Bug #4171 <https://redmine.postgresql.org/issues/4171>`_ - Fix issue where reverse engineered SQL was failing for foreign tables, if it had "=" in the options.
| `Bug #4195 <https://redmine.postgresql.org/issues/4195>`_ - Fix keyboard navigation in "inner" tabsets such as the Query Tool and Debugger. | `Bug #4195 <https://redmine.postgresql.org/issues/4195>`_ - Fix keyboard navigation in "inner" tabsets such as the Query Tool and Debugger.
| `Bug #4228 <https://redmine.postgresql.org/issues/4228>`_ - Ensure the correct label is used in panel headers when viewing filtered rows. | `Bug #4228 <https://redmine.postgresql.org/issues/4228>`_ - Ensure the correct label is used in panel headers when viewing filtered rows.
@ -35,4 +36,5 @@ Bug fixes
| `Bug #4350 <https://redmine.postgresql.org/issues/4350>`_ - Ensure we include the CSRF token when uploading files. | `Bug #4350 <https://redmine.postgresql.org/issues/4350>`_ - Ensure we include the CSRF token when uploading files.
| `Bug #4357 <https://redmine.postgresql.org/issues/4357>`_ - Fix connection restoration issue when pgAdmin server is restarted and the page is refreshed. | `Bug #4357 <https://redmine.postgresql.org/issues/4357>`_ - Fix connection restoration issue when pgAdmin server is restarted and the page is refreshed.
| `Bug #4360 <https://redmine.postgresql.org/issues/4360>`_ - Ensure the debugger control buttons are only enabled once initialisation is complete. | `Bug #4360 <https://redmine.postgresql.org/issues/4360>`_ - Ensure the debugger control buttons are only enabled once initialisation is complete.
| `Bug #4365 <https://redmine.postgresql.org/issues/4365>`_ - Fix help links for backup globals and backup server. | `Bug #4365 <https://redmine.postgresql.org/issues/4365>`_ - Fix help links for backup globals and backup server.
| `Bug #4367 <https://redmine.postgresql.org/issues/4367>`_ - Fix an XSS issue seen in View/Edit data mode if a column name includes HTML.

View File

@ -36,7 +36,8 @@ class CheckForXssFeatureTest(BaseFeatureTest):
def before(self): def before(self):
test_utils.create_table( test_utils.create_table(
self.server, self.test_db, self.test_table_name self.server, self.test_db, self.test_table_name,
['"<script>alert(1)</script>" char']
) )
# This is needed to test dependents tab (eg: BackGrid) # This is needed to test dependents tab (eg: BackGrid)
test_utils.create_constraint( test_utils.create_constraint(
@ -66,6 +67,11 @@ class CheckForXssFeatureTest(BaseFeatureTest):
self._check_xss_in_query_tool_history() self._check_xss_in_query_tool_history()
self.page.close_query_tool() self.page.close_query_tool()
# Query tool view/edit data
self.page.open_view_data(self.test_table_name)
self._check_xss_view_data()
self.page.close_data_grid()
# Explain module # Explain module
self.page.open_query_tool() self.page.open_query_tool()
self._check_xss_in_explain_module() self._check_xss_in_explain_module()
@ -233,6 +239,26 @@ class CheckForXssFeatureTest(BaseFeatureTest):
self.page.click_tab('Query Editor') self.page.click_tab('Query Editor')
def _check_xss_view_data(self):
print(
"\n\tChecking the SlickGrid cell for the XSS",
file=sys.stderr, end=""
)
self.page.find_by_css_selector(".slick-header-column")
cells = self.driver.\
find_elements_by_css_selector(".slick-header-column")
# remove first element as it is row number.
# currently 4th col
source_code = cells[4].get_attribute('innerHTML')
self._check_escaped_characters(
source_code,
'&lt;script&gt;alert(1)&lt;/script&gt;',
"View Data (SlickGrid)"
)
def _check_xss_in_explain_module(self): def _check_xss_in_explain_module(self):
print( print(
"\n\tChecking the Graphical Explain plan for the XSS ...", "\n\tChecking the Graphical Explain plan for the XSS ...",

View File

@ -11,6 +11,7 @@
import os import os
import pickle import pickle
import sys import sys
import re
import simplejson as json import simplejson as json
from flask import Response, url_for, render_template, session, request, \ from flask import Response, url_for, render_template, session, request, \
@ -478,7 +479,7 @@ def poll(trans_id):
if not st: if not st:
return internal_server_error(types) return internal_server_error(types)
for col_info in columns.values(): for col_name, col_info in columns.items():
for col_type in types: for col_type in types:
if col_type['oid'] == col_info['type_code']: if col_type['oid'] == col_info['type_code']:
typname = col_type['typname'] typname = col_type['typname']
@ -487,6 +488,10 @@ def poll(trans_id):
col_info['type_name'] = typname col_info['type_name'] = typname
# Using characters %, (, ) in the argument names is not
# supported in psycopg2
col_info['pgadmin_alias'] = \
re.sub("[%()]+", "|", col_name)
session_obj['columns_info'] = columns session_obj['columns_info'] = columns
# status of async_fetchmany_2darray is True and result is none # status of async_fetchmany_2darray is True and result is none
# means nothing to fetch # means nothing to fetch

View File

@ -677,6 +677,11 @@ class TableCommand(GridCommand):
list_of_sql = {} list_of_sql = {}
_rowid = None _rowid = None
pgadmin_alias = {
col_name: col_info['pgadmin_alias']
for col_name, col_info in columns_info
.items()
}
if conn.connected(): if conn.connected():
# Start the transaction # Start the transaction
@ -745,6 +750,7 @@ class TableCommand(GridCommand):
sql = render_template( sql = render_template(
"/".join([self.sql_path, 'insert.sql']), "/".join([self.sql_path, 'insert.sql']),
data_to_be_saved=column_data, data_to_be_saved=column_data,
pgadmin_alias=pgadmin_alias,
primary_keys=None, primary_keys=None,
object_name=self.object_name, object_name=self.object_name,
nsp_name=self.nsp_name, nsp_name=self.nsp_name,
@ -774,11 +780,17 @@ class TableCommand(GridCommand):
list_of_sql[of_type] = [] list_of_sql[of_type] = []
for each_row in changed_data[of_type]: for each_row in changed_data[of_type]:
data = changed_data[of_type][each_row]['data'] data = changed_data[of_type][each_row]['data']
pk = changed_data[of_type][each_row]['primary_keys'] pk_escaped = {
pk: pk_val.replace('%', '%%')
for pk, pk_val in
changed_data[of_type][each_row]['primary_keys']
.items()
}
sql = render_template( sql = render_template(
"/".join([self.sql_path, 'update.sql']), "/".join([self.sql_path, 'update.sql']),
data_to_be_saved=data, data_to_be_saved=data,
primary_keys=pk, pgadmin_alias=pgadmin_alias,
primary_keys=pk_escaped,
object_name=self.object_name, object_name=self.object_name,
nsp_name=self.nsp_name, nsp_name=self.nsp_name,
data_type=column_type data_type=column_type
@ -831,17 +843,14 @@ class TableCommand(GridCommand):
for opr, sqls in list_of_sql.items(): for opr, sqls in list_of_sql.items():
for item in sqls: for item in sqls:
if item['sql']: if item['sql']:
item['data'] = {
pgadmin_alias[k] if k in pgadmin_alias else k: v
for k, v in item['data'].items()
}
row_added = None row_added = None
# Fetch oids/primary keys def failure_handle():
if 'select_sql' in item and item['select_sql']:
status, res = conn.execute_dict(
item['sql'], item['data'])
else:
status, res = conn.execute_void(
item['sql'], item['data'])
if not status:
conn.execute_void('ROLLBACK;') conn.execute_void('ROLLBACK;')
# If we roll backed every thing then update the # If we roll backed every thing then update the
# message for each sql query. # message for each sql query.
@ -861,6 +870,21 @@ class TableCommand(GridCommand):
return status, res, query_res, _rowid return status, res, query_res, _rowid
try:
# Fetch oids/primary keys
if 'select_sql' in item and item['select_sql']:
status, res = conn.execute_dict(
item['sql'], item['data'])
else:
status, res = conn.execute_void(
item['sql'], item['data'])
except Exception as _:
failure_handle()
raise
if not status:
return failure_handle()
# Select added row from the table # Select added row from the table
if 'select_sql' in item: if 'select_sql' in item:
status, sel_res = conn.execute_dict( status, sel_res = conn.execute_dict(

View File

@ -783,7 +783,7 @@ define('tools.querytool', [
pos: c.pos, pos: c.pos,
field: c.name, field: c.name,
name: c.label, name: c.label,
display_name: c.display_name, display_name: _.escape(c.display_name),
column_type: c.column_type, column_type: c.column_type,
column_type_internal: c.column_type_internal, column_type_internal: c.column_type_internal,
not_null: c.not_null, not_null: c.not_null,
@ -794,7 +794,7 @@ define('tools.querytool', [
// Get the columns width based on longer string among data type or // Get the columns width based on longer string among data type or
// column name. // column name.
var column_type = c.column_type.trim(); var column_type = c.column_type.trim();
var label = c.name.length > column_type.length ? c.name : column_type; var label = c.name.length > column_type.length ? _.escape(c.display_name) : column_type;
if (_.isUndefined(column_size[table_name][c.name])) { if (_.isUndefined(column_size[table_name][c.name])) {
options['width'] = SqlEditorUtils.calculateColumnWidth(label); options['width'] = SqlEditorUtils.calculateColumnWidth(label);

View File

@ -4,7 +4,7 @@ INSERT INTO {{ conn|qtIdent(nsp_name, object_name) }} (
{% if not loop.first %}, {% endif %}{{ conn|qtIdent(col) }}{% endfor %} {% if not loop.first %}, {% endif %}{{ conn|qtIdent(col) }}{% endfor %}
) VALUES ( ) VALUES (
{% for col in data_to_be_saved %} {% for col in data_to_be_saved %}
{% if not loop.first %}, {% endif %}%({{ col }})s::{{ data_type[col] }}{% endfor %} {% if not loop.first %}, {% endif %}%({{ pgadmin_alias[col] }})s::{{ data_type[col] }}{% endfor %}
) )
{% if pk_names and not has_oids %} returning {{pk_names}}{% endif %} {% if pk_names and not has_oids %} returning {{pk_names}}{% endif %}
{% if has_oids %} returning oid{% endif %}; {% if has_oids %} returning oid{% endif %};

View File

@ -1,7 +1,7 @@
{# Update the row with primary keys (specified in primary_keys) #} {# Update the row with primary keys (specified in primary_keys) #}
UPDATE {{ conn|qtIdent(nsp_name, object_name) }} SET UPDATE {{ conn|qtIdent(nsp_name, object_name) }} SET
{% for col in data_to_be_saved %} {% for col in data_to_be_saved %}
{% if not loop.first %}, {% endif %}{{ conn|qtIdent(col) }} = %({{ col }})s::{{ data_type[col] }}{% endfor %} {% if not loop.first %}, {% endif %}{{ conn|qtIdent(col) }} = %({{ pgadmin_alias[col] }})s::{{ data_type[col] }}{% endfor %}
WHERE WHERE
{% for pk in primary_keys %} {% for pk in primary_keys %}
{% if not loop.first %} AND {% endif %}{{ conn|qtIdent(pk) }} = {{ primary_keys[pk]|qtLiteral }}{% endfor %}; {% if not loop.first %} AND {% endif %}{{ conn|qtIdent(pk) }} = {{ primary_keys[pk]|qtLiteral }}{% endfor %};

View File

@ -196,6 +196,9 @@ class DictCursor(_cursor):
Execute function Execute function
""" """
self._odt_desc = None self._odt_desc = None
if params is not None and len(params) == 0:
params = None
return _cursor.execute(self, query, params) return _cursor.execute(self, query, params)
def executemany(self, query, params=None): def executemany(self, query, params=None):

View File

@ -97,6 +97,28 @@ class PgadminPage:
self.find_by_partial_link_text("Query Tool").click() self.find_by_partial_link_text("Query Tool").click()
def open_view_data(self, table_name):
self.driver.find_element_by_link_text("Object").click()
ActionChains(
self.driver
).move_to_element(
self.driver.find_element_by_link_text("View/Edit Data")
).perform()
self.find_by_partial_link_text("All Rows").click()
time.sleep(1)
# wait until datagrid frame is loaded.
self.click_tab(table_name)
WebDriverWait(self.driver, self.timeout).until(
EC.visibility_of_element_located(
(By.CSS_SELECTOR, 'iframe')
), "Timed out waiting for div element to appear"
)
self.driver.switch_to.frame(
self.driver.find_element_by_tag_name('iframe')
)
def enable_menu_item(self, menu_item, wait_time): def enable_menu_item(self, menu_item, wait_time):
start_time = time.time() start_time = time.time()
# wait until menu becomes enabled. # wait until menu becomes enabled.

View File

@ -144,7 +144,7 @@ def create_database(server, db_name, encoding=None):
traceback.print_exc(file=sys.stderr) traceback.print_exc(file=sys.stderr)
def create_table(server, db_name, table_name): def create_table(server, db_name, table_name, extra_columns=[]):
""" """
This function create the table in given database name This function create the table in given database name
:param server: server details :param server: server details
@ -166,18 +166,25 @@ def create_table(server, db_name, table_name):
) )
old_isolation_level = connection.isolation_level old_isolation_level = connection.isolation_level
connection.set_isolation_level(0) connection.set_isolation_level(0)
extra_columns_sql = ", " + ", ".join(extra_columns) \
if len(extra_columns) > 0 else ''
pg_cursor = connection.cursor() pg_cursor = connection.cursor()
pg_cursor.execute( pg_cursor.execute(
'''CREATE TABLE "%s" (some_column VARCHAR, value NUMERIC, '''CREATE TABLE "%s" (some_column VARCHAR, value NUMERIC,
details VARCHAR)''' % table_name) details VARCHAR%s)''' % (table_name, extra_columns_sql))
pg_cursor.execute( pg_cursor.execute(
'''INSERT INTO "%s" VALUES ('Some-Name', 6, 'some info')''' '''INSERT INTO "%s"(some_column, value, details)
VALUES ('Some-Name', 6, 'some info')'''
% table_name) % table_name)
pg_cursor.execute( pg_cursor.execute(
'''INSERT INTO "%s" VALUES ('Some-Other-Name', 22, '''INSERT INTO "%s"(some_column, value, details)
VALUES ('Some-Other-Name', 22,
'some other info')''' % table_name) 'some other info')''' % table_name)
pg_cursor.execute( pg_cursor.execute(
'''INSERT INTO "%s" VALUES ('Yet-Another-Name', 14, '''INSERT INTO "%s"(some_column, value, details)
VALUES ('Yet-Another-Name', 14,
'cool info')''' % table_name) 'cool info')''' % table_name)
connection.set_isolation_level(old_isolation_level) connection.set_isolation_level(old_isolation_level)