Loading...
墨滴

eZy

2021/10/20  阅读:48  主题:默认主题

(双坑)MySQL & Redis 客户端相关的两个 Bug

目录


本文不仅包含对 Bug 本身的描述,还包括以下内容:

  • 随机密码生成功能 demo(python)。
  • MySQL 密码复杂度校验规则说明。
  • expect with redis-cli 自动化实践脚本 demo。

Redis 客户端相关 Bug

Bug 复现

复现流程:

  • bash shell 内复现
[root@7c656d93a89a ~]# grep -i ^requirepass /etc/redis.conf
requirepass "abc123\g"
[root@7c656d93a89a ~]# redis-cli -a "abc123\g" info keyspace
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
NOAUTH Authentication required.
[root@7c656d93a89a ~]# redis-cli -a "abc123\\g" info keyspace
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
NOAUTH Authentication required.
[root@7c656d93a89a ~]# redis-cli -a "abc123\\\g" info keyspace
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
NOAUTH Authentication required.
[root@7c656d93a89a ~]# redis-cli -a 'abc123''\''g' info keyspace
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
NOAUTH Authentication required.

# 交互式命令中可正确解析密码
[root@7c656d93a89a ~]# redis-cli
127.0.0.1:6379> auth "abc123\g"
OK
  • Python 内复现

Python 版本:3.9.1 redis-py 版本:3.5.3

from redis import StrictRedis
from urllib.parse import quote

host = '127.0.0.1'
port = 16379
password = r'abc123\g'
db = 0

encoded_password = quote(password, safe="")
r_session = StrictRedis.from_url(f'redis://:{encoded_password}@{host}:{port}',
                                 db=db,
                                 encoding='UTF-8', charset='UTF-8',
                                 decode_responses=True)

try:
    ping_retval = r_session.ping()
    print(f'ping result: {ping_retval}.')
finally:
    r_session.close()

Bug 概述:

当 redis 密码串包含 "\" 转义字符时,客户端不能正确解析密码。但在 redis-cli 交互模式中,auth 命令可正确解析密码。注意:auth 后密码串要用双引号包裹。

对于已有环境,密码若包含转义符,尽量修改密码。如果不能修改密码,可使用 expect 胶水语言黏连 redis-cli 实现非交互式请求(如监控应用场景),示例如下:

redis_pass='abc123\g'
redis_cli_abs_path='/usr/bin/redis-cli'

escape_str4expect() {
  echo "${*}" |
    sed -e 's/\\/\\\\/g' \
      -e 's/\[/\\[/g'
}


expect <<EOF
set timeout 2

spawn ${redis_cli_abs_path}
expect eof

send "auth \"$(escape_str4expect ${redis_pass})\"\r"
expect eof

send "info\r"
expect eof

send "exit\r"
expect eof

exit
EOF

关于 expect 的使用说明,可参见我之前写过的一篇文章:

  • 外部链接:https://mp.weixin.qq.com/s/o8GWIIseqLx617x1m9L7GQ
  • 内部连接:

对于新下发实例,建议使用以下功能生成随机密码。

随机密码生成功能示例(Python)

  • 逻辑/流程图
  • 代码部分
# coding=utf-8
# ------------------------------------------------------------------------
# Title           : irandompass.py
# Description     : This script will generate random passwords.
# Author          : eZy90
# Last Modified   : 2021/10/14
# Version         : 0.1 beta
# Notes           :
#                 :  保证 - 至少有 2 个大写,2 个小写,2 个特殊字符,2 个数字;
#                 :  特殊字符可定制,避免出现类似生成 redis 密码时出现 [ 符号。
# ------------------------------------------------------------------------
##########################################################################
# 模块导入

import random
import string

##########################################################################
# 对象定义

in_pass_length = int(input("请输入密码长度(默认 16):"or 16)

in_min_uppers = int(input("指定至少包含几个大写字母(默认 2):"or 2)
in_min_lowers = int(input("指定至少包含几个小写字母(默认 2):"or 2)
in_min_digits = int(input("指定至少包含几个数字(默认 2):"or 2)
in_min_swords = int(input("指定至少包含几个特殊字符(默认 4):"or 4)

in_special_chars = input("特殊字符指定(默认 【~!@#$%^&*()_】):"or '~!@#$%^&*()_'

in_gen_many = int(input("生成几个密码(默认 9):"or 9)


class SpecialTotalLenTooMax(Exception):
    def __init__(self, err='指定的最小符号长度总和大于总密码长度!'):
        Exception.__init__(self, err)


def radom_pass(pass_length=in_pass_length,
               min_uppers=in_min_uppers, min_lowers=in_min_lowers,
               min_swords=in_min_swords,
               min_digits=in_min_digits,
               special_chars=in_special_chars)
:

    l_chars = string.ascii_lowercase
    u_chars = string.ascii_uppercase
    d_chars = string.digits

    # debug
    # print(pass_length, min_uppers, min_lowers, min_swords, min_digits)
    if min_uppers + min_lowers + min_swords + min_digits > pass_length:
        raise SpecialTotalLenTooMax

    l_tmp_pass = [
        random.choice(l_chars + u_chars + d_chars)
        for _ in range(pass_length)
    ]

    random_idx = random.sample(
        range(0, pass_length),
        min_uppers + min_lowers + min_swords + min_digits
    )

    min_uppers_idx = random_idx[0:min_uppers]
    min_lower_idx = random_idx[min_uppers:min_uppers + min_lowers]
    min_swords_idx = random_idx[min_uppers + min_lowers:min_uppers + min_lowers + min_swords]
    min_digits_idx = random_idx[min_uppers + min_lowers + min_swords:]
    # debug
    # print(random_idx, min_uppers_idx, min_lower_idx, min_swords_idx, min_digits_idx)

    for _idx in min_uppers_idx:
        tmp_char = random.choice(u_chars)
        l_tmp_pass[_idx] = tmp_char
    for _idx in min_lower_idx:
        tmp_char = random.choice(l_chars)
        l_tmp_pass[_idx] = tmp_char
    for _idx in min_swords_idx:
        tmp_char = random.choice(special_chars)
        l_tmp_pass[_idx] = tmp_char
    for _idx in min_digits_idx:
        tmp_char = random.choice(d_chars)
        l_tmp_pass[_idx] = tmp_char

    gen_pass = ''.join(l_tmp_pass)
    return gen_pass


##########################################################################
# 脚本主体

if __name__ == '__main__':

    banner = f'共计生成 {in_gen_many} 个密码'
    print()
    print(f'{banner:*^66}\n')

    for idx in range(in_gen_many):
        print(radom_pass())

    footer = f'结束'
    print()
    print(f'{footer:*^66}')

此功能不仅限于生成 redis 密码,也可适配 MySQL 相关密码校验策略,策略对应参数注解见下文。

MySQL 密码校验策略相关参数注解

| audit_validate_checksum              | ON     |
| validate_password_length             | 8      |
| validate_password_mixed_case_count   | 1      |
| validate_password_number_count       | 1      |
| validate_password_policy             | MEDIUM |
| validate_password_special_char_count | 1      |
  • audit_validate_checksum:是否开启密码复杂度校验。
  • validate_password_length:密码最小长度。
  • validate_password_mixed_case_count:强制混合大小写,大小写最小字符数。
  • validate_password_number_count:数字最小数量。
  • validate_password_policy:密码复杂度(高、中、低)。
  • validate_password_special_char_count:特殊字符最小数量。

避坑指南

  • 已分配实例,无法修改密码的:使用 expect 胶水语言 + 官方客户端实现非交互式请求。
  • 新下发实例:使用【随机密码生成功能示例】章节部分提到的相关功能生成密码,特殊字符部分排除 "\" 符号。

MySQL 命令行客户端:mysql 的 Bug

Bug 复现

mysql 5.7.31 中也存在类似问题,且比 5.7.33 更难解决。

root@4f998a6e1044:/# mysql --version
mysql  Ver 14.14 Distrib 5.7.33, for Linux (x86_64) using  EditLine wrapper
root@4f998a6e1044:/# echo '中国'
中国

root@4f998a6e1044:/# mysql -uroot -p -e 'select "中国"'
Enter password:
+--------+
| 中国   |
+--------+
| 中国   |
+--------+


# 进入 mysql 交互式命令行,将以上命令中对应 sql 复制到命令行中。
root@4f998a6e1044:/# mysql -uroot -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 5
Server version: 5.7.33 MySQL Community Server (GPL)

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> set names utf8mb4;
Query OK, 0 rows affected (0.01 sec)

-- 中文被自动消除😂
mysql> select "";
+--+
|  |
+--+
|  |
+--+
1 row in set (0.00 sec)


# 定位到参数文件中对应 mysql/client 节,去除参数 default-character-set

root@4f998a6e1044:/# tail -2 /etc/mysql/my.cnf
[mysql]
# default-character-set=utf8mb4
root@4f998a6e1044:/# mysql -uroot -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 6
Server version: 5.7.33 MySQL Community Server (GPL)

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

-- 可正常输入中文😁
mysql> select '中国';
+--------+
| 中国   |
+--------+
| 中国   |
+--------+
1 row in set (0.00 sec)

Bug 概述:

在 mysql 5.7.33 版本中,若参数文件内 mysql/client 节指定了 default-character-set 参数,交互式命令中无法包含中文。

这个 bug 还是比较危险的,大部分公司还保持着手动变更的习惯,有些 DBA 经常交互式的粘贴 sql 文本至 mysql 命令行内,这种操作没有任何问题。但如果 mysql 数据库版本正好是 5.7.33,且参数文件中对应节指定了 default-character-set 参数,很容易造成实际变更与期望不符,且回退困难。

避坑指南

  • 首选解决方案(不适用 5.7.31 版本):参数文件中对应 mysql/client 节不指定 default-character-set 参数,进入命令行后手动设定字符集:set names utf8mb4。
  • 备选解决方案:从官方下载 5.7 lastest stable 二进制包,解压后,将 mysql 命令替换掉。
  • 较折腾解决方案(需要评估兼容性):直接升级数据库至 5.7 lastest stable 版本。

碎碎念(不建议跳过)

给数据库变更操作人员的一点建议

MySQL 的这个 Bug 还是比较严重的,而且资历越深的 DBA 越容易在 mysql/client 节点配置默认字符集相关参数,即越容易掉到这个坑里。所以建议我们在做完变更时必须做详细的校验操作,包括 DBA 在数据库层面及开发人员在业务层面做好验证。毕竟你也看到了,有时候就算是官方工具,也不能完全信任之。

至此,全文结束,负责变更操作的同学加油!


公众号改变了推送规则,若你还想看到我的文章,请给本文 点赞、在看、分享 三连,新文才会第一时间出现推送到你的微信中。


长按下图二维码关注我,相信每篇文章都会给你带来收获。


往期推荐


要面包,也要六便士😁

eZy

2021/10/20  阅读:48  主题:默认主题

作者介绍

eZy