fix(frontend): keep node-reported values one URL path segment

InboundInfoModal put a client's email into /panel/api/clients/ips and
clearIps unencoded, and the host mutations did the same with a host
group id. Both values reach the master from a node's sync without the
panel's own character rules, so "../" and "?" in them chose the route
and query of the admin's authenticated POST; the IP-log request fires as
soon as the window opens. Encode them as every other client route in
the SPA already does.
This commit is contained in:
MHSanaei
2026-11-01 00:08:35 +01:00
parent daf5b16eda
commit d3df3e9ea7
3 changed files with 79 additions and 6 deletions
+8 -3
View File
@@ -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();
},
@@ -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;
@@ -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(<InboundInfoModal open onClose={() => {}} 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 }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
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}`,
]);
});
});