Python如何在紀錄檔中隱藏明文密碼

2023-10-13 18:01:29

Python如何在紀錄檔中隱藏明文密碼

前言

在專案開發中,有的時候會遇到一些安全需求,用以提升程式整體的安全性,提高外來非法攻擊的門檻,而在紀錄檔中隱藏明文密碼列印便是最典型的安全需求之一。

在Python中,明文密碼往往發生於命令執行引數、debug紀錄檔、依賴庫列印等場景中。對於程式自身的明文密碼列印,很輕易地就能通過修改相應程式碼行的方式修復,而對於非程式自身列印,比如依賴庫、外部命令等,則比較棘手,無法通過直接修改程式碼的方式解決。其實,在Python中,logging紀錄檔模組提供了一些自定義方法以過濾特定字串,絕大多數的Python程式均使用logging模組作為其紀錄檔記錄系統,如果開發者已經得知相關明文密碼列印的規則,且使用logging模組記錄紀錄檔,那麼使用在logging模組中過濾特定字串的方法不失為一個很好的選擇。

概念

logging紀錄檔模組是python的一個內建模組,該模組定義了一些函數和類,為上層應用程式或庫實現了一個強大而又靈活的紀錄檔記錄系統。

logging模組將紀錄檔的處理分為四個層次,分別是:

  • logger:logger向上層應用程式暴露介面,程式通過呼叫logger列印紀錄檔,比如logger.info,logger.error等等;
  • handler:handler用於將logger建立的紀錄檔記錄輸出至適合的目的地,比如標準輸出、錯誤、檔案等;
  • filter:filter對如何將紀錄檔記錄輸出提供了更細粒度的控制;
  • formatter:formatter指定了最終紀錄檔記錄輸出的格式。

如上,filter以及formatter層次均提供了對紀錄檔行為擴充套件的手段,針對明文密碼列印問題,我們可以通過自定義filter或者formatter,使用特定規則過濾明文密碼欄位的方式實現。

LogRecord

LogRecord是紀錄檔的基本單元,每次應用程式呼叫Logger列印紀錄檔時,logging模組都會自動建立一個LogRecord範例,其記錄了紀錄檔文字、引數、模組、行數乃至程序ID、執行緒ID等等有用的資訊。

>>> type(record)
<class 'logging.LogRecord'>
>>> record.msg
'password=123456 %s %s'
>>> record.args
('1', '2')
>>> record.created
1697184354.6492243
>>> record.levelname
'INFO'
>>> record.name
'__main__'
>>> record.process
200

上面列出了一些LogRecord物件的屬性,這些屬性大部分也同樣是最後格式化紀錄檔輸出的引數。

filter

filter一般用作匹配並過濾部分紀錄檔,判斷匹配條件的紀錄檔是否允許列印,它提供了一個filter方法,使用布林值作為返回值,如果返回true則表示允許列印,否則表示不允許。

filter方法以LogRecord作為引數,這也表示除了過濾指定紀錄檔的功能以外,也能夠對紀錄檔做更精細的控制。

class Filter(object):
    """
    Filter instances are used to perform arbitrary filtering of LogRecords.
    """
    def filter(self, record: LogRecord) -> bool:
        """
        Determine if the specified record is to be logged.

        Returns True if the record should be logged, or False otherwise.
        If deemed appropriate, the record may be modified in-place.
        """

formatter

formatter負責將LogRecord轉化為最終的輸出字串,它主要是使用args來渲染msg,除此之外,如果LogRecord包含異常堆疊,那麼也會列印出來。

formatter方法以LogRecord作為引數,並返回渲染處理後的字串,當自定義formatter類時,我們能夠既能夠處理渲染前的LogRecord,也能修改渲染後的字串。

class Formatter(object):
    """
    Formatter instances are used to convert a LogRecord to text.
    """
    def format(self, record: LogRecord) -> str:
        """
        Format the specified record as text.

        The record's attribute dictionary is used as the operand to a
        string formatting operation which yields the returned string.
        Before formatting the dictionary, a couple of preparatory steps
        are carried out. The message attribute of the record is computed
        using LogRecord.getMessage(). If the formatting string uses the
        time (as determined by a call to usesTime(), formatTime() is
        called to format the event time. If there is exception information,
        it is formatted using formatException() and appended to the message.
        """

使用formatter實現明文密碼隱藏

import re
import logging
import logging.config

# 自定義formatter類
class SensitiveFormatter(logging.Formatter):
    """Formatter that removes sensitive information in urls."""
    @staticmethod
    def _mask_passwd(s) -> str:
        return re.sub(r'(?<=password=)\S+', r'***', s)

    def format(self, record) -> str:
        s = super().format(record)
        return self._mask_passwd(s)

LOGGING_CONFIG = {
    "version": 1,
    "formatters": {
        "default": {
            "()": SensitiveFormatter,
            "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
        }
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "default",
            "stream": "ext://sys.stdout"
        },
    },
    "loggers": {},
    "root": {
        "level": "DEBUG",
        "handlers": [
            "console",
        ]
    }
}

logging.config.dictConfig(LOGGING_CONFIG)
LOG = logging.getLogger(__name__)

LOG.info('password=123456')
# 2023-10-13 16:58:50,443 - __main__ - INFO - password=***

使用filter實現明文密碼隱藏

import re
import logging
import logging.config

# 自定義filter類
class SensitiveFilter(logging.Filter):
    def __init__(self, patterns):
        super().__init__()
        self._patterns = patterns

    def _mask(self, msg):
        if not isinstance(msg, str):
            return msg
        for pattern in self._patterns:
               msg = re.sub(pattern, r'***', msg)
        return msg

    def filter(self, record):
        record.msg = self._mask(record.msg)
        if isinstance(record.args, dict):
            for k in record.args.keys():
                record.args[k] = self._mask(record.args[k])
        elif isinstance(record.args, tuple):
            record.args = tuple(self._mask(arg) for arg in record.args)
        return super().filter(record)

LOGGING_CONFIG = {
    "version": 1,
    "filters": {
        "default": {
            "()": SensitiveFilter,
            "patterns": [
                r'(?<=password=)\S+',
            ],
        },
    },
    "formatters": {
        "default": {
            "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
        }
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "default",
            "filters": [
                "default",
            ],
            "stream": "ext://sys.stdout"
        },
    },
    "loggers": {},
    "root": {
        "level": "DEBUG",
        "handlers": [
            "console",
        ]
    }
}

logging.config.dictConfig(LOGGING_CONFIG)
LOG = logging.getLogger(__name__)

LOG.info('password=123456')
# 2023-10-13 16:59:22,545 - __main__ - INFO - password=***

附錄

Hiding Sensitive Data from Logs with Python (relaxdiego.com)

logging — Logging facility for Python — Python 3.12.0 documentation