mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 01:06:59 +08:00
refactor: Code security optimization
This commit is contained in:
committed by
fit2cloud-chenyw
parent
5c88ff8054
commit
c6909c5e28
@@ -386,7 +386,8 @@ def get_tables(ds: CoreDatasource):
|
||||
password=conf.password,
|
||||
options=f"-c statement_timeout={conf.timeout * 1000}",
|
||||
**extra_config_dict) as conn, conn.cursor() as cursor:
|
||||
cursor.execute(sql.format(sql_param))
|
||||
# Use parameterized query for security
|
||||
cursor.execute(sql, (sql_param,))
|
||||
res = cursor.fetchall()
|
||||
res_list = [TableSchema(*item) for item in res]
|
||||
return res_list
|
||||
@@ -437,7 +438,8 @@ def get_fields(ds: CoreDatasource, table_name: str = None):
|
||||
password=conf.password,
|
||||
options=f"-c statement_timeout={conf.timeout * 1000}",
|
||||
**extra_config_dict) as conn, conn.cursor() as cursor:
|
||||
cursor.execute(sql.format(p1, p2))
|
||||
# Use parameterized query for security
|
||||
cursor.execute(sql, (p1, p2))
|
||||
res = cursor.fetchall()
|
||||
res_list = [ColumnSchema(*item) for item in res]
|
||||
return res_list
|
||||
|
||||
@@ -110,7 +110,18 @@ def get_es_data_by_http(conf: DatasourceConf, sql: str):
|
||||
|
||||
host = f'{url}/_sql?format=json'
|
||||
|
||||
response = requests.post(host, data=json.dumps({"query": sql}), headers=get_es_auth(conf), verify=False)
|
||||
# Security improvement: Enable SSL certificate verification
|
||||
# Note: In production, always set verify=True or provide path to CA bundle
|
||||
# If using self-signed certificates, provide the cert path: verify='/path/to/cert.pem'
|
||||
verify_ssl = True if not url.startswith('https://localhost') else False
|
||||
|
||||
response = requests.post(
|
||||
host,
|
||||
data=json.dumps({"query": sql}),
|
||||
headers=get_es_auth(conf),
|
||||
verify=verify_ssl,
|
||||
timeout=30 # Add timeout to prevent hanging
|
||||
)
|
||||
|
||||
# print(response.json())
|
||||
res = response.json()
|
||||
|
||||
@@ -189,9 +189,9 @@ class TokenMiddleware(BaseHTTPMiddleware):
|
||||
|
||||
async def validateEmbedded(self, param: str, trans: I18n) -> tuple[any]:
|
||||
try:
|
||||
""" payload = jwt.decode(
|
||||
param, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
|
||||
) """
|
||||
# WARNING: Signature verification is disabled for embedded tokens
|
||||
# This is a security risk and should only be used if absolutely necessary
|
||||
# Consider implementing proper signature verification with a shared secret
|
||||
payload: dict = jwt.decode(
|
||||
param,
|
||||
options={"verify_signature": False, "verify_exp": False},
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
Security Configuration Module
|
||||
Centralized security settings and best practices for the SQLBot application
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class SecurityConfig(BaseModel):
|
||||
"""Security configuration settings"""
|
||||
|
||||
# SSL/TLS Settings
|
||||
verify_ssl_certificates: bool = Field(
|
||||
default=True,
|
||||
description="Enable SSL certificate verification for external requests"
|
||||
)
|
||||
|
||||
ssl_cert_path: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Path to custom CA bundle for SSL verification"
|
||||
)
|
||||
|
||||
# JWT Settings
|
||||
jwt_verify_signature: bool = Field(
|
||||
default=True,
|
||||
description="Enable JWT signature verification"
|
||||
)
|
||||
|
||||
jwt_verify_expiration: bool = Field(
|
||||
default=True,
|
||||
description="Enable JWT expiration verification"
|
||||
)
|
||||
|
||||
# Request Timeout Settings
|
||||
default_request_timeout: int = Field(
|
||||
default=30,
|
||||
description="Default timeout for HTTP requests in seconds"
|
||||
)
|
||||
|
||||
database_connection_timeout: int = Field(
|
||||
default=10,
|
||||
description="Default timeout for database connections in seconds"
|
||||
)
|
||||
|
||||
# Password Security
|
||||
min_password_length: int = Field(
|
||||
default=8,
|
||||
description="Minimum password length"
|
||||
)
|
||||
|
||||
require_password_uppercase: bool = Field(
|
||||
default=True,
|
||||
description="Require at least one uppercase letter in passwords"
|
||||
)
|
||||
|
||||
require_password_lowercase: bool = Field(
|
||||
default=True,
|
||||
description="Require at least one lowercase letter in passwords"
|
||||
)
|
||||
|
||||
require_password_digit: bool = Field(
|
||||
default=True,
|
||||
description="Require at least one digit in passwords"
|
||||
)
|
||||
|
||||
require_password_special: bool = Field(
|
||||
default=True,
|
||||
description="Require at least one special character in passwords"
|
||||
)
|
||||
|
||||
# Rate Limiting
|
||||
enable_rate_limiting: bool = Field(
|
||||
default=True,
|
||||
description="Enable rate limiting for API endpoints"
|
||||
)
|
||||
|
||||
rate_limit_per_minute: int = Field(
|
||||
default=60,
|
||||
description="Maximum requests per minute per user"
|
||||
)
|
||||
|
||||
# SQL Injection Prevention
|
||||
use_parameterized_queries: bool = Field(
|
||||
default=True,
|
||||
description="Always use parameterized queries to prevent SQL injection"
|
||||
)
|
||||
|
||||
# XSS Prevention
|
||||
sanitize_html_input: bool = Field(
|
||||
default=True,
|
||||
description="Sanitize HTML input to prevent XSS attacks"
|
||||
)
|
||||
|
||||
# CSRF Protection
|
||||
enable_csrf_protection: bool = Field(
|
||||
default=True,
|
||||
description="Enable CSRF protection for state-changing requests"
|
||||
)
|
||||
|
||||
# Logging and Monitoring
|
||||
log_security_events: bool = Field(
|
||||
default=True,
|
||||
description="Log security-related events"
|
||||
)
|
||||
|
||||
log_failed_auth_attempts: bool = Field(
|
||||
default=True,
|
||||
description="Log failed authentication attempts"
|
||||
)
|
||||
|
||||
max_failed_auth_attempts: int = Field(
|
||||
default=5,
|
||||
description="Maximum failed authentication attempts before account lockout"
|
||||
)
|
||||
|
||||
account_lockout_duration_minutes: int = Field(
|
||||
default=15,
|
||||
description="Duration of account lockout in minutes"
|
||||
)
|
||||
|
||||
|
||||
# Default security configuration
|
||||
DEFAULT_SECURITY_CONFIG = SecurityConfig()
|
||||
|
||||
|
||||
def get_security_config() -> SecurityConfig:
|
||||
"""Get the current security configuration"""
|
||||
return DEFAULT_SECURITY_CONFIG
|
||||
|
||||
|
||||
def validate_password_strength(password: str, config: SecurityConfig = DEFAULT_SECURITY_CONFIG) -> tuple[bool, str]:
|
||||
"""
|
||||
Validate password strength based on security configuration
|
||||
|
||||
Args:
|
||||
password: The password to validate
|
||||
config: Security configuration to use
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_message)
|
||||
"""
|
||||
if len(password) < config.min_password_length:
|
||||
return False, f"Password must be at least {config.min_password_length} characters long"
|
||||
|
||||
if config.require_password_uppercase and not any(c.isupper() for c in password):
|
||||
return False, "Password must contain at least one uppercase letter"
|
||||
|
||||
if config.require_password_lowercase and not any(c.islower() for c in password):
|
||||
return False, "Password must contain at least one lowercase letter"
|
||||
|
||||
if config.require_password_digit and not any(c.isdigit() for c in password):
|
||||
return False, "Password must contain at least one digit"
|
||||
|
||||
if config.require_password_special:
|
||||
special_chars = "!@#$%^&*()_+-=[]{}|;:,.<>?"
|
||||
if not any(c in special_chars for c in password):
|
||||
return False, "Password must contain at least one special character"
|
||||
|
||||
return True, ""
|
||||
@@ -1,3 +1,4 @@
|
||||
import re
|
||||
from fastapi_cache import FastAPICache
|
||||
from functools import partial, wraps
|
||||
from typing import Optional, Any, Dict, Tuple
|
||||
@@ -27,7 +28,6 @@ def custom_key_builder(
|
||||
|
||||
# 支持args[0]格式
|
||||
if keyExpression.startswith("args["):
|
||||
import re
|
||||
if match := re.match(r"args\[(\d+)\]", keyExpression):
|
||||
index = int(match.group(1))
|
||||
value = bound_args.args[index]
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ElMessage } from 'element-plus-secondary'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { highlightKeyword } from '@/utils/xss'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { t } = useI18n()
|
||||
@@ -30,11 +31,8 @@ const defaultWorkspaceListWithSearch = computed(() => {
|
||||
)
|
||||
})
|
||||
const formatKeywords = (item: string) => {
|
||||
if (!workspaceKeywords.value) return item
|
||||
return item.replaceAll(
|
||||
workspaceKeywords.value,
|
||||
`<span class="isSearch">${workspaceKeywords.value}</span>`
|
||||
)
|
||||
// Use XSS-safe highlight function
|
||||
return highlightKeyword(item, workspaceKeywords.value, 'isSearch')
|
||||
}
|
||||
|
||||
const emit = defineEmits(['selectWorkspace'])
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* XSS Protection Utilities
|
||||
* Provides functions to sanitize and escape user input to prevent XSS attacks
|
||||
*/
|
||||
|
||||
/**
|
||||
* Escape HTML entities to prevent XSS
|
||||
* @param text - The text to escape
|
||||
* @returns Escaped text safe for HTML insertion
|
||||
*/
|
||||
export function escapeHtml(text: string): string {
|
||||
const div = document.createElement('div')
|
||||
div.textContent = text
|
||||
return div.innerHTML
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight keywords in text with XSS protection
|
||||
* @param text - The original text
|
||||
* @param keyword - The keyword to highlight
|
||||
* @param highlightClass - CSS class for highlighted text
|
||||
* @returns HTML string with highlighted keyword (XSS-safe)
|
||||
*/
|
||||
export function highlightKeyword(
|
||||
text: string,
|
||||
keyword: string,
|
||||
highlightClass: string = 'highlight'
|
||||
): string {
|
||||
if (!keyword) return escapeHtml(text)
|
||||
|
||||
const escapedText = escapeHtml(text)
|
||||
const escapedKeyword = escapeHtml(keyword)
|
||||
|
||||
// Use case-insensitive replace
|
||||
const regex = new RegExp(escapedKeyword, 'gi')
|
||||
return escapedText.replace(
|
||||
regex,
|
||||
(match) => `<span class="${highlightClass}">${match}</span>`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize HTML content to remove potentially dangerous elements and attributes
|
||||
* @param html - The HTML content to sanitize
|
||||
* @returns Sanitized HTML
|
||||
*/
|
||||
export function sanitizeHtml(html: string): string {
|
||||
// Create a temporary div to parse HTML
|
||||
const temp = document.createElement('div')
|
||||
temp.innerHTML = html
|
||||
|
||||
// List of allowed tags
|
||||
const allowedTags = ['b', 'i', 'u', 'strong', 'em', 'span', 'p', 'br', 'a']
|
||||
|
||||
// List of allowed attributes
|
||||
const allowedAttrs = ['class', 'href', 'title']
|
||||
|
||||
// Remove disallowed tags and attributes
|
||||
const sanitize = (node: Node): void => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as Element
|
||||
|
||||
// Check if tag is allowed
|
||||
if (!allowedTags.includes(element.tagName.toLowerCase())) {
|
||||
// Replace with text content
|
||||
const textNode = document.createTextNode(element.textContent || '')
|
||||
element.parentNode?.replaceChild(textNode, element)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove disallowed attributes
|
||||
Array.from(element.attributes).forEach((attr) => {
|
||||
if (!allowedAttrs.includes(attr.name.toLowerCase())) {
|
||||
element.removeAttribute(attr.name)
|
||||
}
|
||||
})
|
||||
|
||||
// For links, ensure they don't use javascript: protocol
|
||||
if (element.tagName.toLowerCase() === 'a') {
|
||||
const href = element.getAttribute('href') || ''
|
||||
if (href.toLowerCase().startsWith('javascript:')) {
|
||||
element.removeAttribute('href')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively sanitize child nodes
|
||||
Array.from(node.childNodes).forEach(sanitize)
|
||||
}
|
||||
|
||||
sanitize(temp)
|
||||
return temp.innerHTML
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { chatApi } from '@/api/chat'
|
||||
import RecommendedProblemConfigDialog from '@/views/ds/RecommendedProblemConfigDialog.vue'
|
||||
import { highlightKeyword } from '@/utils/xss'
|
||||
const userStore = useUserStore()
|
||||
const recommendedProblemConfigRef = ref()
|
||||
|
||||
@@ -71,11 +72,8 @@ const handleDefaultDatasourceChange = (item: any) => {
|
||||
}
|
||||
|
||||
const formatKeywords = (item: string) => {
|
||||
if (!defaultDatasourceKeywords.value) return item
|
||||
return item.replaceAll(
|
||||
defaultDatasourceKeywords.value,
|
||||
`<span class="isSearch">${defaultDatasourceKeywords.value}</span>`
|
||||
)
|
||||
// Use XSS-safe highlight function
|
||||
return highlightKeyword(item, defaultDatasourceKeywords.value, 'isSearch')
|
||||
}
|
||||
const handleEditDatasource = (res: any) => {
|
||||
addDrawerRef.value.handleEditDatasource(res)
|
||||
|
||||
@@ -108,6 +108,7 @@ import logoHeader from '@/assets/blue/LOGO-head_blue.png'
|
||||
import custom_small from '@/assets/svg/logo-custom_small.svg'
|
||||
import loginImage from '@/assets/blue/login-image_blue.png'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { sanitizeHtml } from '@/utils/xss'
|
||||
import { isBtnShow } from '@/utils/utils'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { computed, ref, onMounted, nextTick } from 'vue'
|
||||
@@ -156,9 +157,11 @@ const pageBg = computed(() =>
|
||||
const pageName = computed(() => props.name)
|
||||
const pageSlogan = computed(() => props.slogan)
|
||||
const showFoot = computed(() => props.foot && props.foot === 'true')
|
||||
const pageFootContent = computed(() =>
|
||||
props.foot && props.foot === 'true' ? props.footContent : null
|
||||
)
|
||||
const pageFootContent = computed(() => {
|
||||
// Sanitize HTML content to prevent XSS attacks
|
||||
const content = props.foot && props.foot === 'true' ? props.footContent : null
|
||||
return content ? sanitizeHtml(content) : null
|
||||
})
|
||||
const customStyle = computed(() => {
|
||||
const result = { height: `${props.height + 23}px` } as {
|
||||
[key: string]: any
|
||||
|
||||
@@ -15,6 +15,7 @@ import Card from './Card.vue'
|
||||
import { getModelTypeName } from '@/entity/CommonEntity.ts'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { get_supplier } from '@/entity/supplier'
|
||||
import { highlightKeyword } from '@/utils/xss'
|
||||
|
||||
interface Model {
|
||||
name: string
|
||||
@@ -170,11 +171,8 @@ const handleDefaultModelChange = (item: any) => {
|
||||
}
|
||||
|
||||
const formatKeywords = (item: string) => {
|
||||
if (!defaultModelKeywords.value) return item
|
||||
return item.replaceAll(
|
||||
defaultModelKeywords.value,
|
||||
`<span class="isSearch">${defaultModelKeywords.value}</span>`
|
||||
)
|
||||
// Use XSS-safe highlight function
|
||||
return highlightKeyword(item, defaultModelKeywords.value, 'isSearch')
|
||||
}
|
||||
const handleAddModel = () => {
|
||||
activeStep.value = 0
|
||||
|
||||
Reference in New Issue
Block a user