Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions backend/apps/db/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion backend/apps/db/es_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
6 changes: 3 additions & 3 deletions backend/apps/system/middleware/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
160 changes: 160 additions & 0 deletions backend/common/core/security_config.py
Original file line number Diff line number Diff line change
@@ -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, ""
2 changes: 1 addition & 1 deletion backend/common/core/sqlbot_cache.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
from fastapi_cache import FastAPICache
from functools import partial, wraps
from typing import Optional, Any, Dict, Tuple
Expand Down Expand Up @@ -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]
Expand Down
8 changes: 3 additions & 5 deletions frontend/src/components/layout/Workspace.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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'])
Expand Down
93 changes: 93 additions & 0 deletions frontend/src/utils/xss.ts
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 3 additions & 5 deletions frontend/src/views/ds/Datasource.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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)
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/views/system/appearance/LoginPreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
Loading