diff --git a/frontend/src/api/queries/useHostMutations.ts b/frontend/src/api/queries/useHostMutations.ts index dfac21944..a71697f6d 100644 --- a/frontend/src/api/queries/useHostMutations.ts +++ b/frontend/src/api/queries/useHostMutations.ts @@ -20,14 +20,19 @@ export function useHostMutations() { const updateMut = useMutation({ mutationFn: ({ groupId, payload }: { groupId: string; payload: BulkAddHostValues }) => - HttpUtil.post(`/panel/api/hosts/update/${groupId}`, payload, JSON_HEADERS), + HttpUtil.post( + `/panel/api/hosts/update/${encodeURIComponent(groupId)}`, + payload, + JSON_HEADERS, + ), onSuccess: (msg) => { if (msg?.success) invalidate(); }, }); const removeMut = useMutation({ - mutationFn: (groupId: string) => HttpUtil.post(`/panel/api/hosts/del/${groupId}`), + mutationFn: (groupId: string) => + HttpUtil.post(`/panel/api/hosts/del/${encodeURIComponent(groupId)}`), onSuccess: (msg) => { if (msg?.success) invalidate(); }, @@ -35,7 +40,7 @@ export function useHostMutations() { const setEnableMut = useMutation({ mutationFn: ({ groupId, enable }: { groupId: string; enable: boolean }) => - HttpUtil.post(`/panel/api/hosts/setEnable/${groupId}`, { enable }), + HttpUtil.post(`/panel/api/hosts/setEnable/${encodeURIComponent(groupId)}`, { enable }), onSuccess: (msg) => { if (msg?.success) invalidate(); }, diff --git a/frontend/src/pages/inbounds/info/InboundInfoModal.tsx b/frontend/src/pages/inbounds/info/InboundInfoModal.tsx index 4e5554f82..299bf688d 100644 --- a/frontend/src/pages/inbounds/info/InboundInfoModal.tsx +++ b/frontend/src/pages/inbounds/info/InboundInfoModal.tsx @@ -69,7 +69,9 @@ export default function InboundInfoModal({ if (!clientStats?.email) return; setRefreshing(true); try { - const msg = await HttpUtil.post(`/panel/api/clients/ips/${clientStats.email}`); + const msg = await HttpUtil.post( + `/panel/api/clients/ips/${encodeURIComponent(clientStats.email)}`, + ); if (!msg?.success) { setClientIpsText((msg?.obj as string) || 'No IP record'); setClientIpsArray([]); @@ -101,7 +103,9 @@ export default function InboundInfoModal({ const clearClientIps = useCallback(async () => { if (!clientStats?.email) return; - const msg = await HttpUtil.post(`/panel/api/clients/clearIps/${clientStats.email}`); + const msg = await HttpUtil.post( + `/panel/api/clients/clearIps/${encodeURIComponent(clientStats.email)}`, + ); if (msg?.success) { setClientIpsArray([]); setClientIpsText(t('tgbot.noIpRecord')); @@ -223,7 +227,8 @@ export default function InboundInfoModal({ setClientIpsText(''); if (ipLimitEnable && (clientSet?.limitIp ?? 0) > 0 && stats?.email) { - void HttpUtil.post(`/panel/api/clients/ips/${stats.email}`).then((msg) => { + const email = encodeURIComponent(stats.email); + void HttpUtil.post(`/panel/api/clients/ips/${email}`).then((msg) => { if (!msg?.success) { setClientIpsText((msg?.obj as string) || 'No IP record'); return; diff --git a/frontend/src/test/node-reported-url-segments.test.tsx b/frontend/src/test/node-reported-url-segments.test.tsx new file mode 100644 index 000000000..f1dc24fcd --- /dev/null +++ b/frontend/src/test/node-reported-url-segments.test.tsx @@ -0,0 +1,63 @@ +import type { ReactNode } from 'react'; +import { act, render, renderHook } from '@testing-library/react'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { useHostMutations } from '@/api/queries/useHostMutations'; +import { BulkAddHostSchema } from '@/schemas/api/host'; +import { DBInbound } from '@/models/dbinbound'; +import InboundInfoModal from '@/pages/inbounds/info/InboundInfoModal'; +import { makeTestQueryClient } from '@/test/test-utils'; +import { HttpUtil } from '@/utils'; + +// Client emails and host group ids adopted from a node skip the panel's own +// character rules: unencoded, "../" and "?" re-target the admin's POST. +const hostile = '../../setting/update?tgBotEnable=true'; +const encoded = encodeURIComponent(hostile); + +describe('node-reported values stay one URL path segment', () => { + const post = vi.mocked(HttpUtil.post); + + beforeEach(() => { + post.mockClear(); + }); + + it('encodes the client email in the inbound info IP-log request', () => { + const dbInbound = new DBInbound({ + id: 2, + protocol: 'http', + port: 44301, + settings: JSON.stringify({ clients: [{ email: hostile, enable: true, limitIp: 1 }] }), + streamSettings: JSON.stringify({ network: 'tcp', security: 'none' }), + sniffing: '', + clientStats: [{ email: hostile, enable: true, up: 1, down: 1, total: 0, expiryTime: 0 }], + }); + + render( {}} dbInbound={dbInbound} ipLimitEnable />); + + expect(post).toHaveBeenCalledWith(`/panel/api/clients/ips/${encoded}`); + }); + + it('encodes the host group id in the host mutations', async () => { + const queryClient = makeTestQueryClient(); + const wrapper = ({ children }: { children: ReactNode }) => ( + {children} + ); + const { result } = renderHook(() => useHostMutations(), { wrapper }); + + await act(async () => { + await result.current.update( + hostile, + BulkAddHostSchema.parse({ inboundIds: [1], remark: 'cdn' }), + ); + await result.current.setEnable(hostile, false); + await result.current.remove(hostile); + }); + + expect(post.mock.calls.map(([url]) => url)).toEqual([ + `/panel/api/hosts/update/${encoded}`, + `/panel/api/hosts/setEnable/${encoded}`, + `/panel/api/hosts/del/${encoded}`, + ]); + }); +});