feat(models): 组织、用户、角色、客户、会话与消息模型
按组织边界定义数据关系,补齐仓储层与通用工具,迁移脚本可重复执行。
This commit is contained in:
1 parent
2aee6286d6
commit
5e0615f36d
126 files changed
+17436
No files matched your search
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
Copyright 2024-2026 ChatterMate
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import re
|
||||
import html
|
||||
import urllib.parse
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def decode_entities(text: str) -> str:
|
||||
"""Decode HTML entities and URL encoding to catch obfuscated attacks"""
|
||||
# Decode HTML entities multiple times to catch nested encoding
|
||||
for _ in range(3):
|
||||
text = html.unescape(text)
|
||||
|
||||
# Decode URL encoding multiple times
|
||||
for _ in range(3):
|
||||
try:
|
||||
text = urllib.parse.unquote(text)
|
||||
except Exception:
|
||||
break
|
||||
|
||||
return text
|
||||
|
||||
|
||||
def sanitize_message(message: Optional[str]) -> Optional[str]:
|
||||
"""
|
||||
Comprehensive sanitization to prevent XSS attacks including encoded payloads.
|
||||
|
||||
Blocks:
|
||||
- All dangerous HTML tags (script, iframe, object, embed, svg, etc.)
|
||||
- JavaScript URIs (including encoded variants)
|
||||
- Data URIs (except data:image)
|
||||
- Event handlers (all on* attributes)
|
||||
- SVG-based XSS vectors
|
||||
- Encoded payloads (HTML entities, URL encoding, Unicode)
|
||||
- CSS expressions and behaviors
|
||||
- Base64 encoded attacks
|
||||
- Template literals and ES6 syntax
|
||||
|
||||
Args:
|
||||
message: The message text to sanitize
|
||||
|
||||
Returns:
|
||||
Sanitized message text or None if input is None
|
||||
"""
|
||||
if not message:
|
||||
return message
|
||||
|
||||
# SECURITY LAYER 1: Remove markdown link and image syntax to prevent injection
|
||||
# This must happen BEFORE marked.js processes it on the frontend
|
||||
# Pattern matches: [text](url) including nested brackets
|
||||
message = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', message)
|
||||
|
||||
# SECURITY: Remove markdown image syntax  - capture alt text without the !
|
||||
message = re.sub(r'!\[([^\]]*)\]\([^\)]+\)', r'\1', message)
|
||||
|
||||
# SECURITY: Remove reference-style markdown links [text][ref]
|
||||
message = re.sub(r'\[([^\]]+)\]\[[^\]]+\]', r'\1', message)
|
||||
|
||||
# SECURITY: Remove link reference definitions [ref]: url
|
||||
message = re.sub(r'^\[[^\]]+\]:\s*.+$', '', message, flags=re.MULTILINE)
|
||||
|
||||
# SECURITY LAYER 2: Decode any encoded content to catch obfuscated attacks
|
||||
decoded_message = decode_entities(message)
|
||||
|
||||
# Check for encoded javascript: or data: URIs in decoded content
|
||||
dangerous_patterns = [
|
||||
r'javascript\s*:',
|
||||
r'data\s*:(?!image/)',
|
||||
r'vbscript\s*:',
|
||||
r'file\s*:',
|
||||
r'about\s*:',
|
||||
]
|
||||
|
||||
for pattern in dangerous_patterns:
|
||||
if re.search(pattern, decoded_message, re.IGNORECASE):
|
||||
# If found in decoded content, sanitize the original too
|
||||
message = re.sub(pattern, '', message, flags=re.IGNORECASE)
|
||||
|
||||
# Remove all dangerous HTML tags (comprehensive list)
|
||||
# SECURITY: Including <a> and <img> to prevent any external resource loading or clickable links
|
||||
dangerous_tags = [
|
||||
'script', 'iframe', 'frame', 'frameset', 'object', 'embed',
|
||||
'applet', 'base', 'link', 'meta', 'style', 'svg', 'math',
|
||||
'form', 'input', 'button', 'textarea', 'select', 'option',
|
||||
'xml', 'xss', 'import', 'video', 'audio', 'track', 'source',
|
||||
'canvas', 'details', 'template', 'slot', 'noscript',
|
||||
'marquee', 'bgsound', 'keygen', 'command',
|
||||
'a', 'img', 'area', 'map' # Block links and images to prevent external resource loading
|
||||
]
|
||||
|
||||
for tag in dangerous_tags:
|
||||
# Remove opening and closing tags with any attributes
|
||||
message = re.sub(rf'<{tag}[^>]*>.*?</{tag}>', '', message, flags=re.IGNORECASE | re.DOTALL)
|
||||
message = re.sub(rf'<{tag}[^>]*/?>', '', message, flags=re.IGNORECASE)
|
||||
|
||||
# Remove javascript: URIs (including encoded variants)
|
||||
message = re.sub(r'j\s*a\s*v\s*a\s*s\s*c\s*r\s*i\s*p\s*t\s*:', '', message, flags=re.IGNORECASE)
|
||||
message = re.sub(r'&#[x]?[0-9a-f]+;?', lambda m: decode_entities(m.group()), message)
|
||||
|
||||
# Remove vbscript: URIs
|
||||
message = re.sub(r'vbscript:', '', message, flags=re.IGNORECASE)
|
||||
|
||||
# Remove data: URIs that could contain HTML/JS (but preserve data:image)
|
||||
message = re.sub(r'data:(?!image/)[^,;]*[,;]', '', message, flags=re.IGNORECASE)
|
||||
|
||||
# Remove expression() and behavior CSS properties
|
||||
message = re.sub(r'expression\s*\([^)]*\)', '', message, flags=re.IGNORECASE)
|
||||
message = re.sub(r'behavior\s*:[^;]*', '', message, flags=re.IGNORECASE)
|
||||
message = re.sub(r'-moz-binding\s*:', '', message, flags=re.IGNORECASE)
|
||||
|
||||
# Remove import statements
|
||||
message = re.sub(r'@import', '', message, flags=re.IGNORECASE)
|
||||
|
||||
# Remove all event handlers (comprehensive list)
|
||||
event_handlers = [
|
||||
'onabort', 'onactivate', 'onafterprint', 'onafterupdate', 'onbeforeactivate',
|
||||
'onbeforecopy', 'onbeforecut', 'onbeforedeactivate', 'onbeforeeditfocus',
|
||||
'onbeforepaste', 'onbeforeprint', 'onbeforeunload', 'onbeforeupdate', 'onblur',
|
||||
'onbounce', 'oncellchange', 'onchange', 'onclick', 'oncontextmenu', 'oncontrolselect',
|
||||
'oncopy', 'oncut', 'ondataavailable', 'ondatasetchanged', 'ondatasetcomplete',
|
||||
'ondblclick', 'ondeactivate', 'ondrag', 'ondragend', 'ondragenter', 'ondragleave',
|
||||
'ondragover', 'ondragstart', 'ondrop', 'onerror', 'onerrorupdate', 'onfilterchange',
|
||||
'onfinish', 'onfocus', 'onfocusin', 'onfocusout', 'onhelp', 'oninput', 'oninvalid',
|
||||
'onkeydown', 'onkeypress', 'onkeyup', 'onlayoutcomplete', 'onload', 'onlosecapture',
|
||||
'onmessage', 'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove',
|
||||
'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel', 'onmove', 'onmoveend',
|
||||
'onmovestart', 'onpaste', 'onpropertychange', 'onreadystatechange', 'onreset',
|
||||
'onresize', 'onresizeend', 'onresizestart', 'onrowenter', 'onrowexit', 'onrowsdelete',
|
||||
'onrowsinserted', 'onscroll', 'onsearch', 'onselect', 'onselectionchange',
|
||||
'onselectstart', 'onstart', 'onstop', 'onsubmit', 'onunload',
|
||||
'oncanplay', 'oncanplaythrough', 'oncuechange', 'ondurationchange', 'onemptied',
|
||||
'onended', 'onloadeddata', 'onloadedmetadata', 'onloadstart', 'onpause', 'onplay',
|
||||
'onplaying', 'onprogress', 'onratechange', 'onseeked', 'onseeking', 'onstalled',
|
||||
'onsuspend', 'ontimeupdate', 'onvolumechange', 'onwaiting', 'ontoggle',
|
||||
'onauxclick', 'ongotpointercapture', 'onlostpointercapture', 'onpointercancel',
|
||||
'onpointerdown', 'onpointerenter', 'onpointerleave', 'onpointermove', 'onpointerout',
|
||||
'onpointerover', 'onpointerup', 'onwheel', 'onanimationcancel', 'onanimationend',
|
||||
'onanimationiteration', 'onanimationstart', 'ontransitioncancel', 'ontransitionend',
|
||||
'ontransitionrun', 'ontransitionstart', 'onshow', 'onsort', 'onpointerrawupdate'
|
||||
]
|
||||
|
||||
for handler in event_handlers:
|
||||
# Remove with quotes
|
||||
message = re.sub(rf'{handler}\s*=\s*["\'][^"\']*["\']', '', message, flags=re.IGNORECASE)
|
||||
# Remove without quotes
|
||||
message = re.sub(rf'{handler}\s*=\s*[^>\s]*', '', message, flags=re.IGNORECASE)
|
||||
# Remove with backticks (template literals)
|
||||
message = re.sub(rf'{handler}\s*=\s*`[^`]*`', '', message, flags=re.IGNORECASE)
|
||||
|
||||
# Remove dangerous attributes
|
||||
# SECURITY: Including src, href, and all attributes that can load external resources
|
||||
dangerous_attrs = [
|
||||
'formaction', 'action', 'form', 'srcdoc', 'srcset', 'dynsrc', 'lowsrc',
|
||||
'ping', 'poster', 'background', 'code', 'codebase', 'archive', 'profile',
|
||||
'xmlns', 'xlink:href', 'attributename', 'from', 'to', 'values', 'begin',
|
||||
'autofocus', 'autoplay', 'controls', 'manifest', 'sandbox',
|
||||
'src', 'href', 'data', 'icon', 'usemap' # Block all resource-loading attributes
|
||||
]
|
||||
|
||||
for attr in dangerous_attrs:
|
||||
message = re.sub(rf'{attr}\s*=\s*["\'][^"\']*["\']', '', message, flags=re.IGNORECASE)
|
||||
message = re.sub(rf'{attr}\s*=\s*[^>\s]*', '', message, flags=re.IGNORECASE)
|
||||
|
||||
# Remove HTML comments (can be used to hide malicious code)
|
||||
message = re.sub(r'<!--.*?-->', '', message, flags=re.DOTALL)
|
||||
|
||||
# Remove CDATA sections
|
||||
message = re.sub(r'<!\[CDATA\[.*?\]\]>', '', message, flags=re.DOTALL | re.IGNORECASE)
|
||||
|
||||
# Remove XML processing instructions
|
||||
message = re.sub(r'<\?.*?\?>', '', message, flags=re.DOTALL)
|
||||
|
||||
# Remove null bytes and other control characters that can bypass filters
|
||||
message = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', message)
|
||||
|
||||
# Remove Unicode directional override characters (can hide malicious code)
|
||||
message = re.sub(r'[\u202a-\u202e\u2066-\u2069]', '', message)
|
||||
|
||||
return message
|
||||
Reference in new issue
Block a user