refactor: Code security optimization

This commit is contained in:
xuwei-fit2cloud
2025-12-26 16:38:47 +08:00
committed by fit2cloud-chenyw
parent 5c88ff8054
commit c6909c5e28
10 changed files with 288 additions and 25 deletions
+4 -2
View File
@@ -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
+12 -1
View File
@@ -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()
+3 -3
View File
@@ -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},
+160
View File
@@ -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 -1
View File
@@ -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]
+3 -5
View File
@@ -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'])
+93
View File
@@ -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
}
+3 -5
View File
@@ -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
+3 -5
View File
@@ -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