diff --git a/backend/apps/db/db.py b/backend/apps/db/db.py
index 51084236..33b87534 100644
--- a/backend/apps/db/db.py
+++ b/backend/apps/db/db.py
@@ -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
diff --git a/backend/apps/db/es_engine.py b/backend/apps/db/es_engine.py
index a1c6ff1c..27f46b76 100644
--- a/backend/apps/db/es_engine.py
+++ b/backend/apps/db/es_engine.py
@@ -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()
diff --git a/backend/apps/system/middleware/auth.py b/backend/apps/system/middleware/auth.py
index 41167624..b101f10c 100644
--- a/backend/apps/system/middleware/auth.py
+++ b/backend/apps/system/middleware/auth.py
@@ -136,9 +136,9 @@ async def validateAssistant(self, assistantToken: Optional[str], trans: I18n) ->
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},
diff --git a/backend/common/core/security_config.py b/backend/common/core/security_config.py
new file mode 100644
index 00000000..2b8d38a1
--- /dev/null
+++ b/backend/common/core/security_config.py
@@ -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, ""
diff --git a/backend/common/core/sqlbot_cache.py b/backend/common/core/sqlbot_cache.py
index dda597b1..39e8afb6 100644
--- a/backend/common/core/sqlbot_cache.py
+++ b/backend/common/core/sqlbot_cache.py
@@ -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]
diff --git a/frontend/src/components/layout/Workspace.vue b/frontend/src/components/layout/Workspace.vue
index 3d9f97eb..1f76e92a 100644
--- a/frontend/src/components/layout/Workspace.vue
+++ b/frontend/src/components/layout/Workspace.vue
@@ -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,
- `${workspaceKeywords.value}`
- )
+ // Use XSS-safe highlight function
+ return highlightKeyword(item, workspaceKeywords.value, 'isSearch')
}
const emit = defineEmits(['selectWorkspace'])
diff --git a/frontend/src/utils/xss.ts b/frontend/src/utils/xss.ts
new file mode 100644
index 00000000..c0307d71
--- /dev/null
+++ b/frontend/src/utils/xss.ts
@@ -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) => `${match}`
+ )
+}
+
+/**
+ * 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
+}
diff --git a/frontend/src/views/ds/Datasource.vue b/frontend/src/views/ds/Datasource.vue
index 3a9fcb28..979e5cb1 100644
--- a/frontend/src/views/ds/Datasource.vue
+++ b/frontend/src/views/ds/Datasource.vue
@@ -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,
- `${defaultDatasourceKeywords.value}`
- )
+ // Use XSS-safe highlight function
+ return highlightKeyword(item, defaultDatasourceKeywords.value, 'isSearch')
}
const handleEditDatasource = (res: any) => {
addDrawerRef.value.handleEditDatasource(res)
diff --git a/frontend/src/views/system/appearance/LoginPreview.vue b/frontend/src/views/system/appearance/LoginPreview.vue
index f79dbf6f..7e2ba2d7 100644
--- a/frontend/src/views/system/appearance/LoginPreview.vue
+++ b/frontend/src/views/system/appearance/LoginPreview.vue
@@ -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
diff --git a/frontend/src/views/system/model/Model.vue b/frontend/src/views/system/model/Model.vue
index 4aff07bc..36bb300b 100644
--- a/frontend/src/views/system/model/Model.vue
+++ b/frontend/src/views/system/model/Model.vue
@@ -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,
- `${defaultModelKeywords.value}`
- )
+ // Use XSS-safe highlight function
+ return highlightKeyword(item, defaultModelKeywords.value, 'isSearch')
}
const handleAddModel = () => {
activeStep.value = 0