mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-31 10:14:15 +00:00
fix(settings): reject spaces, '\' and control chars in URI path settings
webBasePath, subPath, subJsonPath and subClashPath are URL paths, so '/' stays allowed, but spaces, backslashes and control characters break routing. Strip them as you type (shared sanitizePath helper, now also applied to the panel base path) and reject them on save in AllSetting.CheckValid so direct API callers are covered too.
This commit is contained in:
parent
2fa7be86dc
commit
a08bb91f58
6 changed files with 76 additions and 25 deletions
|
|
@ -11,6 +11,7 @@ import {
|
||||||
import type { AllSetting } from '@/models/setting';
|
import type { AllSetting } from '@/models/setting';
|
||||||
import { HttpUtil, LanguageManager } from '@/utils';
|
import { HttpUtil, LanguageManager } from '@/utils';
|
||||||
import { SettingListItem } from '@/components/ui';
|
import { SettingListItem } from '@/components/ui';
|
||||||
|
import { sanitizePath } from './uriPath';
|
||||||
|
|
||||||
interface ApiMsg<T = unknown> {
|
interface ApiMsg<T = unknown> {
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
|
|
@ -150,7 +151,7 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
|
||||||
</SettingListItem>
|
</SettingListItem>
|
||||||
|
|
||||||
<SettingListItem paddings="small" title={t('pages.settings.panelUrlPath')} description={t('pages.settings.panelUrlPathDesc')}>
|
<SettingListItem paddings="small" title={t('pages.settings.panelUrlPath')} description={t('pages.settings.panelUrlPathDesc')}>
|
||||||
<Input value={allSetting.webBasePath} onChange={(e) => updateSetting({ webBasePath: e.target.value })} />
|
<Input value={allSetting.webBasePath} onChange={(e) => updateSetting({ webBasePath: sanitizePath(e.target.value) })} />
|
||||||
</SettingListItem>
|
</SettingListItem>
|
||||||
|
|
||||||
<SettingListItem paddings="small" title={t('pages.settings.sessionMaxAge')} description={t('pages.settings.sessionMaxAgeDesc')}>
|
<SettingListItem paddings="small" title={t('pages.settings.sessionMaxAge')} description={t('pages.settings.sessionMaxAgeDesc')}>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { AllSetting } from '@/models/setting';
|
import type { AllSetting } from '@/models/setting';
|
||||||
import { SettingListItem } from '@/components/ui';
|
import { SettingListItem } from '@/components/ui';
|
||||||
|
import { sanitizePath, normalizePath } from './uriPath';
|
||||||
import './SubscriptionFormatsTab.css';
|
import './SubscriptionFormatsTab.css';
|
||||||
|
|
||||||
interface SubscriptionFormatsTabProps {
|
interface SubscriptionFormatsTabProps {
|
||||||
|
|
@ -60,18 +61,6 @@ const directDomainsOptions = [
|
||||||
{ label: 'Google', value: 'geosite:google' },
|
{ label: 'Google', value: 'geosite:google' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function sanitizePath(input: string): string {
|
|
||||||
return String(input ?? '').replace(/[:*]/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePath(input: string): string {
|
|
||||||
let p = input || '/';
|
|
||||||
if (!p.startsWith('/')) p = '/' + p;
|
|
||||||
if (!p.endsWith('/')) p += '/';
|
|
||||||
p = p.replace(/\/+/g, '/');
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readJson<T>(raw: string, fallback: T): T {
|
function readJson<T>(raw: string, fallback: T): T {
|
||||||
try {
|
try {
|
||||||
if (!raw) return fallback;
|
if (!raw) return fallback;
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,13 @@ import { Collapse, Divider, Input, InputNumber, Switch } from 'antd';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { AllSetting } from '@/models/setting';
|
import type { AllSetting } from '@/models/setting';
|
||||||
import { SettingListItem } from '@/components/ui';
|
import { SettingListItem } from '@/components/ui';
|
||||||
|
import { sanitizePath, normalizePath } from './uriPath';
|
||||||
|
|
||||||
interface SubscriptionGeneralTabProps {
|
interface SubscriptionGeneralTabProps {
|
||||||
allSetting: AllSetting;
|
allSetting: AllSetting;
|
||||||
updateSetting: (patch: Partial<AllSetting>) => void;
|
updateSetting: (patch: Partial<AllSetting>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizePath(input: string): string {
|
|
||||||
return String(input ?? '').replace(/[:*]/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePath(input: string): string {
|
|
||||||
let p = input || '/';
|
|
||||||
if (!p.startsWith('/')) p = '/' + p;
|
|
||||||
if (!p.endsWith('/')) p += '/';
|
|
||||||
p = p.replace(/\/+/g, '/');
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SubscriptionGeneralTab({ allSetting, updateSetting }: SubscriptionGeneralTabProps) {
|
export default function SubscriptionGeneralTab({ allSetting, updateSetting }: SubscriptionGeneralTabProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|
|
||||||
17
frontend/src/pages/settings/uriPath.ts
Normal file
17
frontend/src/pages/settings/uriPath.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
export function sanitizePath(input: string): string {
|
||||||
|
let out = '';
|
||||||
|
for (const ch of String(input ?? '')) {
|
||||||
|
const code = ch.charCodeAt(0);
|
||||||
|
if (ch === ':' || ch === '*' || ch === ' ' || ch === '\\' || code < 0x20 || code === 0x7f) continue;
|
||||||
|
out += ch;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizePath(input: string): string {
|
||||||
|
let p = input || '/';
|
||||||
|
if (!p.startsWith('/')) p = '/' + p;
|
||||||
|
if (!p.endsWith('/')) p += '/';
|
||||||
|
p = p.replace(/\/+/g, '/');
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
@ -128,6 +128,15 @@ type AllSettingView struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckValid validates all settings in the AllSetting struct, checking IP addresses, ports, SSL certificates, and other configuration values.
|
// CheckValid validates all settings in the AllSetting struct, checking IP addresses, ports, SSL certificates, and other configuration values.
|
||||||
|
func pathHasForbiddenChar(s string) bool {
|
||||||
|
for _, r := range s {
|
||||||
|
if r == '\\' || r == ' ' || r < 0x20 || r == 0x7f {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (s *AllSetting) CheckValid() error {
|
func (s *AllSetting) CheckValid() error {
|
||||||
if s.WebListen != "" {
|
if s.WebListen != "" {
|
||||||
ip := net.ParseIP(s.WebListen)
|
ip := net.ParseIP(s.WebListen)
|
||||||
|
|
@ -169,6 +178,20 @@ func (s *AllSetting) CheckValid() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, p := range []struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
}{
|
||||||
|
{"web base path", s.WebBasePath},
|
||||||
|
{"subscription path", s.SubPath},
|
||||||
|
{"subscription JSON path", s.SubJsonPath},
|
||||||
|
{"subscription Clash path", s.SubClashPath},
|
||||||
|
} {
|
||||||
|
if pathHasForbiddenChar(p.value) {
|
||||||
|
return common.NewError("URI path contains an invalid character:", p.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(s.WebBasePath, "/") {
|
if !strings.HasPrefix(s.WebBasePath, "/") {
|
||||||
s.WebBasePath = "/" + s.WebBasePath
|
s.WebBasePath = "/" + s.WebBasePath
|
||||||
}
|
}
|
||||||
|
|
|
||||||
32
web/entity/path_validation_test.go
Normal file
32
web/entity/path_validation_test.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestPathHasForbiddenChar(t *testing.T) {
|
||||||
|
valid := []string{
|
||||||
|
"",
|
||||||
|
"/",
|
||||||
|
"/sub/",
|
||||||
|
"/json/",
|
||||||
|
"/a/b/c/",
|
||||||
|
"/My-Path_123/",
|
||||||
|
}
|
||||||
|
for _, p := range valid {
|
||||||
|
if pathHasForbiddenChar(p) {
|
||||||
|
t.Errorf("pathHasForbiddenChar(%q) = true, want false", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invalid := []string{
|
||||||
|
"/sub path/",
|
||||||
|
"/back\\slash/",
|
||||||
|
"/tab\there/",
|
||||||
|
"/new\nline/",
|
||||||
|
"/\x7f/",
|
||||||
|
}
|
||||||
|
for _, p := range invalid {
|
||||||
|
if !pathHasForbiddenChar(p) {
|
||||||
|
t.Errorf("pathHasForbiddenChar(%q) = false, want true", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue