Skip to content

Commit f034c38

Browse files
shussinhzmur
authored andcommitted
Bug#37039394 Backport Bug# 36816986 - MySQL Shell command injection
Problem: mysqldump does not sanitize the SQL statements which might contain some special characters like new line feed etc. Because of this, a user can inject arbitrary shell command(s) in to the dump file created by mysqldump. And if that file is executed on a host machine, the shell command will get executed on that host machine. Solution: The fix is to make sure that the rows which are being fetched from dump_tablespaces are sanitized properly. This fix is just sanitizing the rows coming in from mysql_fetch_row while dumping tablespaces. Change-Id: Ide74636ba60fd3cb6c936149f76b3c4ead70ecc9
1 parent aa9c14a commit f034c38

File tree

3 files changed

+162
-30
lines changed

3 files changed

+162
-30
lines changed

client/mysqldump.c

Lines changed: 133 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
#include <m_string.h>
5555
#include <m_ctype.h>
5656
#include <hash.h>
57+
#include <ctype.h>
5758
#include <stdarg.h>
5859

5960
#include "client_priv.h"
@@ -2328,6 +2329,82 @@ static char const* fix_identifier_with_newline(char const* object_name, my_bool*
23282329
}
23292330

23302331

2332+
/*
2333+
* fprintf_string:
2334+
* -- Print the escaped version of the given char* row into the md_result_file.
2335+
*
2336+
* @param[in] row the row to be printed
2337+
* @param[in] row_len length of the row
2338+
* @param[in] quote quote character, like ' or ` etc.
2339+
* @param[in] needs_newline whether to print newline after the row
2340+
*
2341+
* @retval void
2342+
*
2343+
*/
2344+
static void fprintf_string(char *row, ulong row_len, char quote,
2345+
my_bool needs_newline) {
2346+
// Create the buffer where we'll have sanitized row.
2347+
char buffer[2048];
2348+
char *pbuffer;
2349+
uint64_t curr_row_size;
2350+
pbuffer = &buffer[0];
2351+
2352+
curr_row_size = ((uint64_t)row_len) * 2 + 1;
2353+
2354+
// We'll allocate dynamic memory only for huge rows
2355+
if (curr_row_size > sizeof(buffer))
2356+
pbuffer = (char *)my_malloc(PSI_NOT_INSTRUMENTED, curr_row_size, MYF(0));
2357+
2358+
// Put the sanitized row in the buffer.
2359+
mysql_real_escape_string_quote(mysql, pbuffer, row, row_len, '\'');
2360+
2361+
// Opening quote
2362+
fputc(quote, md_result_file);
2363+
2364+
// Print the row to the file.
2365+
fputs(pbuffer, md_result_file);
2366+
2367+
// Closing quote
2368+
fputc(quote, md_result_file);
2369+
2370+
// Add the new line
2371+
if (needs_newline) fputc('\n', md_result_file);
2372+
2373+
// Free the buffer if we have to.
2374+
if (pbuffer != &buffer[0]) my_free(pbuffer);
2375+
}
2376+
2377+
/*
2378+
* is_string_integer:
2379+
* Check if the given string is a valid integer or not.
2380+
*
2381+
* @param[in] str number to be checked
2382+
* @param[in] str_len length of the string
2383+
*
2384+
* @retval TRUE if the string represents an integer
2385+
* @retval FALSE if the string has non-digit characters
2386+
*/
2387+
static my_bool is_string_integer(const char *str, ulong str_len) {
2388+
ulong start_index;
2389+
ulong i;
2390+
2391+
// Empty strings are invalid numbers
2392+
if (str_len == 0) return FALSE;
2393+
2394+
start_index = 0;
2395+
2396+
// For negative integers, start the index with 1
2397+
if (str[0] == '-') {
2398+
if (str_len == 1) return FALSE;
2399+
start_index = 1;
2400+
}
2401+
2402+
for (i = start_index; i < str_len; i++)
2403+
if (!isdigit(str[i])) return FALSE;
2404+
2405+
return TRUE;
2406+
}
2407+
23312408
/*
23322409
create_delimiter
23332410
Generate a new (null-terminated) string that does not exist in query
@@ -4440,6 +4517,7 @@ static int dump_tablespaces(char* ts_where)
44404517
MYSQL_RES *tableres;
44414518
char buf[FN_REFLEN];
44424519
DYNAMIC_STRING sqlbuf;
4520+
ulong *lengths;
44434521
int first= 0;
44444522
/*
44454523
The following are used for parsing the EXTRA field
@@ -4500,24 +4578,33 @@ static int dump_tablespaces(char* ts_where)
45004578
buf[0]= 0;
45014579
while ((row= mysql_fetch_row(tableres)))
45024580
{
4581+
lengths = mysql_fetch_lengths(tableres);
45034582
if (strcmp(buf, row[0]) != 0)
45044583
first= 1;
45054584
if (first)
45064585
{
4586+
/*
4587+
* The print_comment below prints single line comments in the
4588+
* md_result_file (--). The single line comment is terminated by a new
4589+
* line, however because of the usage of mysql_real_escape_string_quote,
4590+
* the new line character will get escaped too in the string, hence
4591+
* another new line characters are being used at the end of the string
4592+
* to terminate the single line comment.
4593+
*/
4594+
mysql_real_escape_string_quote(mysql, buf, row[0], lengths[0], '\'');
45074595
print_comment(md_result_file, 0, "\n--\n-- Logfile group: %s\n--\n",
4508-
row[0]);
4509-
4596+
buf);
4597+
buf[0] = 0;
45104598
fprintf(md_result_file, "\nCREATE");
45114599
}
45124600
else
45134601
{
45144602
fprintf(md_result_file, "\nALTER");
45154603
}
4516-
fprintf(md_result_file,
4517-
" LOGFILE GROUP %s\n"
4518-
" ADD UNDOFILE '%s'\n",
4519-
row[0],
4520-
row[1]);
4604+
fprintf(md_result_file, " LOGFILE GROUP ");
4605+
fprintf_string(row[0], lengths[0], '`', TRUE);
4606+
fprintf(md_result_file, " ADD UNDOFILE ");
4607+
fprintf_string(row[1], lengths[1], '\'', TRUE);
45214608
if (first)
45224609
{
45234610
ubs= strstr(row[5],extra_format);
@@ -4527,15 +4614,15 @@ static int dump_tablespaces(char* ts_where)
45274614
endsemi= strstr(ubs,";");
45284615
if(endsemi)
45294616
endsemi[0]= '\0';
4617+
if (!is_string_integer(ubs, (ulong)strlen(ubs))) return 1;
45304618
fprintf(md_result_file,
45314619
" UNDO_BUFFER_SIZE %s\n",
45324620
ubs);
45334621
}
4534-
fprintf(md_result_file,
4535-
" INITIAL_SIZE %s\n"
4536-
" ENGINE=%s;\n",
4537-
row[3],
4538-
row[4]);
4622+
if (!is_string_integer(row[3], lengths[3])) return 1;
4623+
fprintf(md_result_file, " INITIAL_SIZE %s\n ENGINE=", row[3]);
4624+
fprintf_string(row[4], lengths[4], '`', FALSE);
4625+
fprintf(md_result_file, ";\n");
45394626
check_io(md_result_file);
45404627
if (first)
45414628
{
@@ -4567,38 +4654,54 @@ static int dump_tablespaces(char* ts_where)
45674654
DBUG_RETURN(1);
45684655
}
45694656

4657+
DBUG_EXECUTE_IF("tablespace_injection_test", {
4658+
mysql_free_result(tableres);
4659+
mysql_query_with_error_report(
4660+
mysql, &tableres,
4661+
"SELECT 'TN; /*' AS TABLESPACE_NAME, 'FN' AS FILE_NAME, 'LGN' AS "
4662+
"LOGFILE_GROUP_NAME, 77 AS EXTENT_SIZE, 88 AS INITIAL_SIZE, "
4663+
"'*/\nsystem touch foo;\n' AS ENGINE");
4664+
});
4665+
45704666
buf[0]= 0;
45714667
while ((row= mysql_fetch_row(tableres)))
45724668
{
4669+
lengths = mysql_fetch_lengths(tableres);
45734670
if (strcmp(buf, row[0]) != 0)
45744671
first= 1;
45754672
if (first)
45764673
{
4577-
print_comment(md_result_file, 0, "\n--\n-- Tablespace: %s\n--\n", row[0]);
4674+
/*
4675+
* The print_comment below prints single line comments in the
4676+
* md_result_file (--). The single line comment is terminated by a new
4677+
* line, however because of the usage of mysql_real_escape_string_quote,
4678+
* the new line character will get escaped too in the string, hence
4679+
* another new line characters are being used at the end of the string
4680+
* to terminate the single line comment.
4681+
*/
4682+
mysql_real_escape_string_quote(mysql, buf, row[0], lengths[0], '\'');
4683+
print_comment(md_result_file, 0, "\n--\n-- Tablespace: %s\n--\n", buf);
4684+
buf[0] = 0;
45784685
fprintf(md_result_file, "\nCREATE");
45794686
}
45804687
else
45814688
{
45824689
fprintf(md_result_file, "\nALTER");
45834690
}
4584-
fprintf(md_result_file,
4585-
" TABLESPACE %s\n"
4586-
" ADD DATAFILE '%s'\n",
4587-
row[0],
4588-
row[1]);
4589-
if (first)
4590-
{
4591-
fprintf(md_result_file,
4592-
" USE LOGFILE GROUP %s\n"
4593-
" EXTENT_SIZE %s\n",
4594-
row[2],
4595-
row[3]);
4691+
fprintf(md_result_file, " TABLESPACE ");
4692+
fprintf_string(row[0], lengths[0], '`', TRUE);
4693+
fprintf(md_result_file, " ADD DATAFILE ");
4694+
fprintf_string(row[1], lengths[1], '\'', TRUE);
4695+
if (first) {
4696+
fprintf(md_result_file, " USE LOGFILE GROUP ");
4697+
fprintf_string(row[2], lengths[2], '`', TRUE);
4698+
if (!is_string_integer(row[3], lengths[3])) return 1;
4699+
fprintf(md_result_file, " EXTENT_SIZE %s\n", row[3]);
45964700
}
4597-
fprintf(md_result_file,
4598-
" INITIAL_SIZE %s\n"
4599-
" ENGINE=%s;\n",
4600-
row[4],
4601-
row[5]);
4701+
if (!is_string_integer(row[4], lengths[4])) return 1;
4702+
fprintf(md_result_file, " INITIAL_SIZE %s\n ENGINE=", row[4]);
4703+
fprintf_string(row[5], lengths[5], '`', FALSE);
4704+
fprintf(md_result_file, ";\n");
46024705
check_io(md_result_file);
46034706
if (first)
46044707
{
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#
2+
# Bug#36816986 - MySQL Shell command injection
3+
#
4+
CREATE DATABASE bug36816986;
5+
-- Run mysqldump with tablespace_injection_test.
6+
The test injected string must be found:
7+
Pattern found.
8+
DROP DATABASE bug36816986;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--source include/mysql_have_debug.inc
2+
3+
--echo #
4+
--echo # Bug#36816986 - MySQL Shell command injection
5+
--echo #
6+
7+
let $grep_file= $MYSQLTEST_VARDIR/tmp/bug36816986.sql;
8+
let $grep_output=boolean;
9+
10+
CREATE DATABASE bug36816986;
11+
12+
--echo -- Run mysqldump with tablespace_injection_test.
13+
--exec $MYSQL_DUMP --debug="d,tablespace_injection_test" --result-file=$grep_file bug36816986 --all-tablespaces 2>&1
14+
15+
--echo The test injected string must be found:
16+
let $grep_pattern=qr| ENGINE=\*/\nsystem touch foo|;
17+
--source include/grep_pattern.inc
18+
19+
# Cleanup
20+
--remove_file $grep_file
21+
DROP DATABASE bug36816986;

0 commit comments

Comments
 (0)