MXS-1774 Reject query with masked columns + functions

The masking filter works only on the result-set. However, if
functions are used, the column names will not be available in
the result-set, and hence masking will not take place.

Now, the statement is checked and if functions are used in
conjunction with columns that should be masked, the statement
is rejected. Thus, functions can no longer be used for bypassing
the masking. That was possible earlier as well, but required
manually setting up the firewall filter.
This commit is contained in:
Johan Wikman 2018-07-02 11:56:49 +03:00
parent 702f8aaed4
commit 43cc6ca3cd
4 changed files with 208 additions and 45 deletions

View File

@ -86,7 +86,7 @@ extern "C" MXS_MODULE* MXS_CREATE_MODULE()
MXS_FILTER_VERSION,
"A masking filter that is capable of masking/obfuscating returned column values.",
"V1.0.0",
RCAP_TYPE_STMT_INPUT | RCAP_TYPE_CONTIGUOUS_OUTPUT,
RCAP_TYPE_CONTIGUOUS_INPUT | RCAP_TYPE_CONTIGUOUS_OUTPUT,
&MaskingFilter::s_object,
NULL, /* Process init. */
NULL, /* Process finish. */

View File

@ -18,6 +18,7 @@
#include <maxscale/buffer.hh>
#include <maxscale/filter.hh>
#include <maxscale/modutil.h>
#include <maxscale/mysql_utils.h>
#include <maxscale/poll.h>
#include <maxscale/protocol/mysql.h>
@ -55,6 +56,68 @@ int MaskingFilterSession::routeQuery(GWBUF* pPacket)
switch (request.command())
{
case MXS_COM_QUERY:
{
m_res.reset(request.command(), m_filter.rules());
SMaskingRules sRules = m_filter.rules();
const char *zUser = session_get_user(m_pSession);
const char *zHost = session_get_remote(m_pSession);
if (!zUser)
{
zUser = "";
}
if (!zHost)
{
zHost = "";
}
auto pred1 = [&sRules, zUser, zHost](const QC_FIELD_INFO& field_info)
{
const MaskingRules::Rule* pRule = sRules->get_rule_for(field_info, zUser, zHost);
return pRule ? true : false;
};
auto pred2 = [&sRules, zUser, zHost, &pred1](const QC_FUNCTION_INFO& function_info)
{
const QC_FIELD_INFO* begin = function_info.fields;
const QC_FIELD_INFO* end = begin + function_info.n_fields;
auto i = std::find_if(begin, end, pred1);
return i != end;
};
const QC_FUNCTION_INFO* pInfos;
size_t nInfos;
qc_get_function_info(pPacket, &pInfos, &nInfos);
const QC_FUNCTION_INFO* begin = pInfos;
const QC_FUNCTION_INFO* end = begin + nInfos;
auto i = std::find_if(begin, end, pred2);
if (i == end)
{
m_state = EXPECTING_RESPONSE;
}
else
{
std::stringstream ss;
ss << "The function " << i->name << " is used in conjunction with a field "
<< "that should be masked for '" << zUser << "'@'" << zHost << "', access is denied.";
GWBUF* pResponse = modutil_create_mysql_err_msg(1, 0, 1141, "HY000", ss.str().c_str());
set_response(pResponse);
m_state = EXPECTING_NOTHING;
}
}
break;
case MXS_COM_STMT_EXECUTE:
m_res.reset(request.command(), m_filter.rules());
m_state = EXPECTING_RESPONSE;
@ -64,7 +127,14 @@ int MaskingFilterSession::routeQuery(GWBUF* pPacket)
m_state = IGNORING_RESPONSE;
}
return FilterSession::routeQuery(pPacket);
int rv = 1;
if (m_state != EXPECTING_NOTHING)
{
rv = FilterSession::routeQuery(pPacket);
}
return rv;
}
int MaskingFilterSession::clientReply(GWBUF* pPacket)

View File

@ -1044,6 +1044,65 @@ string MaskingRules::Rule::match() const
return s;
}
bool MaskingRules::Rule::matches(const ComQueryResponse::ColumnDef& column_def,
const char* zUser,
const char* zHost) const
{
const LEncString& table = column_def.org_table();
const LEncString& database = column_def.schema();
// If the resultset does not contain table and database names, as will
// be the case in e.g. "SELECT * FROM table UNION SELECT * FROM table",
// we consider it a match if a table or database have been provided.
// Otherwise it would be easy to bypass a table/database rule.
bool match =
(m_column == column_def.org_name()) &&
(m_table.empty() || table.empty() || (m_table == table)) &&
(m_database.empty() || database.empty() || (m_database == database));
if (match)
{
// If the column matched, then we need to check whether the rule applies
// to the user and host.
match = matches_account(zUser, zHost);
}
return match;
}
bool MaskingRules::Rule::matches(const QC_FIELD_INFO& field,
const char* zUser,
const char* zHost) const
{
const char* zColumn = field.column;
const char* zTable = field.table;
const char* zDatabase = field.database;
ss_dassert(zColumn);
// If the resultset does not contain table and database names, as will
// be the case in e.g. "SELECT * FROM table UNION SELECT * FROM table",
// we consider it a match if a table or database have been provided.
// Otherwise it would be easy to bypass a table/database rule.
bool match =
(m_column == zColumn) &&
(m_table.empty() || !zTable || (m_table == zTable)) &&
(m_database.empty() || !zDatabase || (m_database == zDatabase));
if (match)
{
// If the column matched, then we need to check whether the rule applies
// to the user and host.
match = matches_account(zUser, zHost);
}
return match;
}
namespace
{
@ -1067,52 +1126,32 @@ private:
}
bool MaskingRules::Rule::matches(const ComQueryResponse::ColumnDef& column_def,
const char* zUser,
const char* zHost) const
bool MaskingRules::Rule::matches_account(const char* zUser,
const char* zHost) const
{
const LEncString& table = column_def.org_table();
const LEncString& database = column_def.schema();
bool match = true;
// If the resultset does not contain table and database names, as will
// be the case in e.g. "SELECT * FROM table UNION SELECT * FROM table",
// we consider it a match if a table or database have been provided.
// Otherwise it would be easy to bypass a table/database rule.
AccountMatcher matcher(zUser, zHost);
bool match =
(m_column == column_def.org_name()) &&
(m_table.empty() || table.empty() || (m_table == table)) &&
(m_database.empty() || database.empty() || (m_database == database));
if (match)
if (m_applies_to.size() != 0)
{
// If the column matched, then we need to check whether the rule applies
// to the user and host.
vector<SAccount>::const_iterator i = std::find_if(m_applies_to.begin(),
m_applies_to.end(),
matcher);
AccountMatcher matcher(zUser, zHost);
match = (i != m_applies_to.end());
}
if (m_applies_to.size() != 0)
{
match = false;
if (match && (m_exempted.size() != 0))
{
// If it is still a match, we need to check whether the user/host is
// exempted.
vector<SAccount>::const_iterator i = std::find_if(m_applies_to.begin(),
m_applies_to.end(),
matcher);
vector<SAccount>::const_iterator i = std::find_if(m_exempted.begin(),
m_exempted.end(),
matcher);
match = (i != m_applies_to.end());
}
if (match && (m_exempted.size() != 0))
{
// If it is still a match, we need to check whether the user/host is
// exempted.
vector<SAccount>::const_iterator i = std::find_if(m_exempted.begin(),
m_exempted.end(),
matcher);
match = (i == m_exempted.end());
}
match = (i == m_exempted.end());
}
return match;
@ -1352,13 +1391,14 @@ std::auto_ptr<MaskingRules> MaskingRules::create_from(json_t* pRoot)
namespace
{
template<class T>
class RuleMatcher : std::unary_function<MaskingRules::SRule, bool>
{
public:
RuleMatcher(const ComQueryResponse::ColumnDef& column_def,
RuleMatcher(const T& field,
const char* zUser,
const char* zHost)
: m_column_def(column_def)
: m_field(field)
, m_zUser(zUser)
, m_zHost(zHost)
{
@ -1366,11 +1406,11 @@ public:
bool operator()(const MaskingRules::SRule& sRule)
{
return sRule->matches(m_column_def, m_zUser, m_zHost);
return sRule->matches(m_field, m_zUser, m_zHost);
}
private:
const ComQueryResponse::ColumnDef& m_column_def;
const T& m_field;
const char* m_zUser;
const char* m_zHost;
};
@ -1383,7 +1423,7 @@ const MaskingRules::Rule* MaskingRules::get_rule_for(const ComQueryResponse::Col
{
const Rule* pRule = NULL;
RuleMatcher matcher(column_def, zUser, zHost);
RuleMatcher<ComQueryResponse::ColumnDef> matcher(column_def, zUser, zHost);
vector<SRule>::const_iterator i = std::find_if(m_rules.begin(), m_rules.end(), matcher);
if (i != m_rules.end())
@ -1395,3 +1435,23 @@ const MaskingRules::Rule* MaskingRules::get_rule_for(const ComQueryResponse::Col
return pRule;
}
const MaskingRules::Rule* MaskingRules::get_rule_for(const QC_FIELD_INFO& field_info,
const char* zUser,
const char* zHost) const
{
const Rule* pRule = NULL;
RuleMatcher<QC_FIELD_INFO> matcher(field_info, zUser, zHost);
vector<SRule>::const_iterator i = std::find_if(m_rules.begin(), m_rules.end(), matcher);
if (i != m_rules.end())
{
const SRule& sRule = *i;
pRule = sRule.get();
}
return pRule;
}

View File

@ -123,6 +123,19 @@ public:
const char* zUser,
const char* zHost) const;
/**
* Establish whether a rule matches a field and user/host.
*
* @param field What field.
* @param zUser The current user.
* @param zHost The current host.
*
* @return True, if the rule matches.
*/
bool matches(const QC_FIELD_INFO& field,
const char* zUser,
const char* zHost) const;
/**
* Mask the column content with value or fill.
*
@ -134,6 +147,9 @@ public:
Rule(const Rule&);
Rule& operator = (const Rule&);
bool matches_account(const char* zUser,
const char* zHost) const;
private:
std::string m_column;
std::string m_table;
@ -361,6 +377,23 @@ public:
const char* zUser,
const char* zHost) const;
/**
* Return the rule object that matches a QC_FIELD_INFO and user/host.
*
* @param field A field.
* @param zUser The current user.
* @param zHost The current host.
*
* @return A rule object that matches the field indo and user/host
* or NULL if no such rule object exists.
*
* @attention The returned object remains value only as long as the
* @c MaskingRules object remains valid.
*/
const Rule* get_rule_for(const QC_FIELD_INFO& field_info,
const char* zUser,
const char* zHost) const;
typedef std::tr1::shared_ptr<Rule> SRule;
private: