Skip to content

Commit c6909c5

Browse files
xuwei-fit2cloudfit2cloud-chenyw
authored andcommitted
refactor: Code security optimization
1 parent 5c88ff8 commit c6909c5

File tree

10 files changed

+288
-25
lines changed

10 files changed

+288
-25
lines changed

backend/apps/db/db.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,8 @@ def get_tables(ds: CoreDatasource):
386386
password=conf.password,
387387
options=f"-c statement_timeout={conf.timeout * 1000}",
388388
**extra_config_dict) as conn, conn.cursor() as cursor:
389-
cursor.execute(sql.format(sql_param))
389+
# Use parameterized query for security
390+
cursor.execute(sql, (sql_param,))
390391
res = cursor.fetchall()
391392
res_list = [TableSchema(*item) for item in res]
392393
return res_list
@@ -437,7 +438,8 @@ def get_fields(ds: CoreDatasource, table_name: str = None):
437438
password=conf.password,
438439
options=f"-c statement_timeout={conf.timeout * 1000}",
439440
**extra_config_dict) as conn, conn.cursor() as cursor:
440-
cursor.execute(sql.format(p1, p2))
441+
# Use parameterized query for security
442+
cursor.execute(sql, (p1, p2))
441443
res = cursor.fetchall()
442444
res_list = [ColumnSchema(*item) for item in res]
443445
return res_list

backend/apps/db/es_engine.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,18 @@ def get_es_data_by_http(conf: DatasourceConf, sql: str):
110110

111111
host = f'{url}/_sql?format=json'
112112

113-
response = requests.post(host, data=json.dumps({"query": sql}), headers=get_es_auth(conf), verify=False)
113+
# Security improvement: Enable SSL certificate verification
114+
# Note: In production, always set verify=True or provide path to CA bundle
115+
# If using self-signed certificates, provide the cert path: verify='/path/to/cert.pem'
116+
verify_ssl = True if not url.startswith('https://localhost') else False
117+
118+
response = requests.post(
119+
host,
120+
data=json.dumps({"query": sql}),
121+
headers=get_es_auth(conf),
122+
verify=verify_ssl,
123+
timeout=30 # Add timeout to prevent hanging
124+
)
114125

115126
# print(response.json())
116127
res = response.json()

backend/apps/system/middleware/auth.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,9 @@ async def validateAssistant(self, assistantToken: Optional[str], trans: I18n) ->
189189

190190
async def validateEmbedded(self, param: str, trans: I18n) -> tuple[any]:
191191
try:
192-
""" payload = jwt.decode(
193-
param, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
194-
) """
192+
# WARNING: Signature verification is disabled for embedded tokens
193+
# This is a security risk and should only be used if absolutely necessary
194+
# Consider implementing proper signature verification with a shared secret
195195
payload: dict = jwt.decode(
196196
param,
197197
options={"verify_signature": False, "verify_exp": False},
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""
2+
Security Configuration Module
3+
Centralized security settings and best practices for the SQLBot application
4+
"""
5+
6+
from pydantic import BaseModel, Field
7+
from typing import Optional
8+
9+
10+
class SecurityConfig(BaseModel):
11+
"""Security configuration settings"""
12+
13+
# SSL/TLS Settings
14+
verify_ssl_certificates: bool = Field(
15+
default=True,
16+
description="Enable SSL certificate verification for external requests"
17+
)
18+
19+
ssl_cert_path: Optional[str] = Field(
20+
default=None,
21+
description="Path to custom CA bundle for SSL verification"
22+
)
23+
24+
# JWT Settings
25+
jwt_verify_signature: bool = Field(
26+
default=True,
27+
description="Enable JWT signature verification"
28+
)
29+
30+
jwt_verify_expiration: bool = Field(
31+
default=True,
32+
description="Enable JWT expiration verification"
33+
)
34+
35+
# Request Timeout Settings
36+
default_request_timeout: int = Field(
37+
default=30,
38+
description="Default timeout for HTTP requests in seconds"
39+
)
40+
41+
database_connection_timeout: int = Field(
42+
default=10,
43+
description="Default timeout for database connections in seconds"
44+
)
45+
46+
# Password Security
47+
min_password_length: int = Field(
48+
default=8,
49+
description="Minimum password length"
50+
)
51+
52+
require_password_uppercase: bool = Field(
53+
default=True,
54+
description="Require at least one uppercase letter in passwords"
55+
)
56+
57+
require_password_lowercase: bool = Field(
58+
default=True,
59+
description="Require at least one lowercase letter in passwords"
60+
)
61+
62+
require_password_digit: bool = Field(
63+
default=True,
64+
description="Require at least one digit in passwords"
65+
)
66+
67+
require_password_special: bool = Field(
68+
default=True,
69+
description="Require at least one special character in passwords"
70+
)
71+
72+
# Rate Limiting
73+
enable_rate_limiting: bool = Field(
74+
default=True,
75+
description="Enable rate limiting for API endpoints"
76+
)
77+
78+
rate_limit_per_minute: int = Field(
79+
default=60,
80+
description="Maximum requests per minute per user"
81+
)
82+
83+
# SQL Injection Prevention
84+
use_parameterized_queries: bool = Field(
85+
default=True,
86+
description="Always use parameterized queries to prevent SQL injection"
87+
)
88+
89+
# XSS Prevention
90+
sanitize_html_input: bool = Field(
91+
default=True,
92+
description="Sanitize HTML input to prevent XSS attacks"
93+
)
94+
95+
# CSRF Protection
96+
enable_csrf_protection: bool = Field(
97+
default=True,
98+
description="Enable CSRF protection for state-changing requests"
99+
)
100+
101+
# Logging and Monitoring
102+
log_security_events: bool = Field(
103+
default=True,
104+
description="Log security-related events"
105+
)
106+
107+
log_failed_auth_attempts: bool = Field(
108+
default=True,
109+
description="Log failed authentication attempts"
110+
)
111+
112+
max_failed_auth_attempts: int = Field(
113+
default=5,
114+
description="Maximum failed authentication attempts before account lockout"
115+
)
116+
117+
account_lockout_duration_minutes: int = Field(
118+
default=15,
119+
description="Duration of account lockout in minutes"
120+
)
121+
122+
123+
# Default security configuration
124+
DEFAULT_SECURITY_CONFIG = SecurityConfig()
125+
126+
127+
def get_security_config() -> SecurityConfig:
128+
"""Get the current security configuration"""
129+
return DEFAULT_SECURITY_CONFIG
130+
131+
132+
def validate_password_strength(password: str, config: SecurityConfig = DEFAULT_SECURITY_CONFIG) -> tuple[bool, str]:
133+
"""
134+
Validate password strength based on security configuration
135+
136+
Args:
137+
password: The password to validate
138+
config: Security configuration to use
139+
140+
Returns:
141+
Tuple of (is_valid, error_message)
142+
"""
143+
if len(password) < config.min_password_length:
144+
return False, f"Password must be at least {config.min_password_length} characters long"
145+
146+
if config.require_password_uppercase and not any(c.isupper() for c in password):
147+
return False, "Password must contain at least one uppercase letter"
148+
149+
if config.require_password_lowercase and not any(c.islower() for c in password):
150+
return False, "Password must contain at least one lowercase letter"
151+
152+
if config.require_password_digit and not any(c.isdigit() for c in password):
153+
return False, "Password must contain at least one digit"
154+
155+
if config.require_password_special:
156+
special_chars = "!@#$%^&*()_+-=[]{}|;:,.<>?"
157+
if not any(c in special_chars for c in password):
158+
return False, "Password must contain at least one special character"
159+
160+
return True, ""

backend/common/core/sqlbot_cache.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
from fastapi_cache import FastAPICache
23
from functools import partial, wraps
34
from typing import Optional, Any, Dict, Tuple
@@ -27,7 +28,6 @@ def custom_key_builder(
2728

2829
# 支持args[0]格式
2930
if keyExpression.startswith("args["):
30-
import re
3131
if match := re.match(r"args\[(\d+)\]", keyExpression):
3232
index = int(match.group(1))
3333
value = bound_args.args[index]

frontend/src/components/layout/Workspace.vue

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ElMessage } from 'element-plus-secondary'
99
import { useI18n } from 'vue-i18n'
1010
import { useRouter } from 'vue-router'
1111
import { useUserStore } from '@/stores/user'
12+
import { highlightKeyword } from '@/utils/xss'
1213
1314
const userStore = useUserStore()
1415
const { t } = useI18n()
@@ -30,11 +31,8 @@ const defaultWorkspaceListWithSearch = computed(() => {
3031
)
3132
})
3233
const formatKeywords = (item: string) => {
33-
if (!workspaceKeywords.value) return item
34-
return item.replaceAll(
35-
workspaceKeywords.value,
36-
`<span class="isSearch">${workspaceKeywords.value}</span>`
37-
)
34+
// Use XSS-safe highlight function
35+
return highlightKeyword(item, workspaceKeywords.value, 'isSearch')
3836
}
3937
4038
const emit = defineEmits(['selectWorkspace'])

frontend/src/utils/xss.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* XSS Protection Utilities
3+
* Provides functions to sanitize and escape user input to prevent XSS attacks
4+
*/
5+
6+
/**
7+
* Escape HTML entities to prevent XSS
8+
* @param text - The text to escape
9+
* @returns Escaped text safe for HTML insertion
10+
*/
11+
export function escapeHtml(text: string): string {
12+
const div = document.createElement('div')
13+
div.textContent = text
14+
return div.innerHTML
15+
}
16+
17+
/**
18+
* Highlight keywords in text with XSS protection
19+
* @param text - The original text
20+
* @param keyword - The keyword to highlight
21+
* @param highlightClass - CSS class for highlighted text
22+
* @returns HTML string with highlighted keyword (XSS-safe)
23+
*/
24+
export function highlightKeyword(
25+
text: string,
26+
keyword: string,
27+
highlightClass: string = 'highlight'
28+
): string {
29+
if (!keyword) return escapeHtml(text)
30+
31+
const escapedText = escapeHtml(text)
32+
const escapedKeyword = escapeHtml(keyword)
33+
34+
// Use case-insensitive replace
35+
const regex = new RegExp(escapedKeyword, 'gi')
36+
return escapedText.replace(
37+
regex,
38+
(match) => `<span class="${highlightClass}">${match}</span>`
39+
)
40+
}
41+
42+
/**
43+
* Sanitize HTML content to remove potentially dangerous elements and attributes
44+
* @param html - The HTML content to sanitize
45+
* @returns Sanitized HTML
46+
*/
47+
export function sanitizeHtml(html: string): string {
48+
// Create a temporary div to parse HTML
49+
const temp = document.createElement('div')
50+
temp.innerHTML = html
51+
52+
// List of allowed tags
53+
const allowedTags = ['b', 'i', 'u', 'strong', 'em', 'span', 'p', 'br', 'a']
54+
55+
// List of allowed attributes
56+
const allowedAttrs = ['class', 'href', 'title']
57+
58+
// Remove disallowed tags and attributes
59+
const sanitize = (node: Node): void => {
60+
if (node.nodeType === Node.ELEMENT_NODE) {
61+
const element = node as Element
62+
63+
// Check if tag is allowed
64+
if (!allowedTags.includes(element.tagName.toLowerCase())) {
65+
// Replace with text content
66+
const textNode = document.createTextNode(element.textContent || '')
67+
element.parentNode?.replaceChild(textNode, element)
68+
return
69+
}
70+
71+
// Remove disallowed attributes
72+
Array.from(element.attributes).forEach((attr) => {
73+
if (!allowedAttrs.includes(attr.name.toLowerCase())) {
74+
element.removeAttribute(attr.name)
75+
}
76+
})
77+
78+
// For links, ensure they don't use javascript: protocol
79+
if (element.tagName.toLowerCase() === 'a') {
80+
const href = element.getAttribute('href') || ''
81+
if (href.toLowerCase().startsWith('javascript:')) {
82+
element.removeAttribute('href')
83+
}
84+
}
85+
}
86+
87+
// Recursively sanitize child nodes
88+
Array.from(node.childNodes).forEach(sanitize)
89+
}
90+
91+
sanitize(temp)
92+
return temp.innerHTML
93+
}

frontend/src/views/ds/Datasource.vue

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { useI18n } from 'vue-i18n'
1818
import { useUserStore } from '@/stores/user'
1919
import { chatApi } from '@/api/chat'
2020
import RecommendedProblemConfigDialog from '@/views/ds/RecommendedProblemConfigDialog.vue'
21+
import { highlightKeyword } from '@/utils/xss'
2122
const userStore = useUserStore()
2223
const recommendedProblemConfigRef = ref()
2324
@@ -71,11 +72,8 @@ const handleDefaultDatasourceChange = (item: any) => {
7172
}
7273
7374
const formatKeywords = (item: string) => {
74-
if (!defaultDatasourceKeywords.value) return item
75-
return item.replaceAll(
76-
defaultDatasourceKeywords.value,
77-
`<span class="isSearch">${defaultDatasourceKeywords.value}</span>`
78-
)
75+
// Use XSS-safe highlight function
76+
return highlightKeyword(item, defaultDatasourceKeywords.value, 'isSearch')
7977
}
8078
const handleEditDatasource = (res: any) => {
8179
addDrawerRef.value.handleEditDatasource(res)

frontend/src/views/system/appearance/LoginPreview.vue

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ import logoHeader from '@/assets/blue/LOGO-head_blue.png'
108108
import custom_small from '@/assets/svg/logo-custom_small.svg'
109109
import loginImage from '@/assets/blue/login-image_blue.png'
110110
import { propTypes } from '@/utils/propTypes'
111+
import { sanitizeHtml } from '@/utils/xss'
111112
import { isBtnShow } from '@/utils/utils'
112113
import { useI18n } from 'vue-i18n'
113114
import { computed, ref, onMounted, nextTick } from 'vue'
@@ -156,9 +157,11 @@ const pageBg = computed(() =>
156157
const pageName = computed(() => props.name)
157158
const pageSlogan = computed(() => props.slogan)
158159
const showFoot = computed(() => props.foot && props.foot === 'true')
159-
const pageFootContent = computed(() =>
160-
props.foot && props.foot === 'true' ? props.footContent : null
161-
)
160+
const pageFootContent = computed(() => {
161+
// Sanitize HTML content to prevent XSS attacks
162+
const content = props.foot && props.foot === 'true' ? props.footContent : null
163+
return content ? sanitizeHtml(content) : null
164+
})
162165
const customStyle = computed(() => {
163166
const result = { height: `${props.height + 23}px` } as {
164167
[key: string]: any

0 commit comments

Comments
 (0)