mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
fix: cap sanitized icon length and re-encode as compact base64
The schema length check ran on the input, but sanitizeMcpIconPath then re-encoded the markup with encodeURIComponent, which expands SVG by well over the base64 input size (a 235KB base64 icon became a 374KB percent string). create stored that over-limit value, and editing the server resubmitted the prefilled value and failed MCPServerUserInputSchema. Re-encode the sanitized SVG as base64 (far more compact for angle-heavy markup, which sanitizing can itself grow via explicit close tags) and drop anything still over MAX_MCP_ICON_PATH_LENGTH, so a stored icon can never exceed the cap that a later edit re-validates against.
This commit is contained in:
parent
1393f92cba
commit
a1f768c083
2 changed files with 42 additions and 6 deletions
|
|
@ -1,7 +1,12 @@
|
|||
import { MAX_MCP_ICON_PATH_LENGTH } from 'librechat-data-provider';
|
||||
import { sanitizeMcpIconPath } from './icons';
|
||||
|
||||
/** Decode the percent-encoded SVG body `sanitizeMcpIconPath` re-emits. */
|
||||
/** Decode the SVG body `sanitizeMcpIconPath` re-emits (base64 or percent-form). */
|
||||
function decode(dataUri: string): string {
|
||||
const base64Prefix = 'data:image/svg+xml;base64,';
|
||||
if (dataUri.startsWith(base64Prefix)) {
|
||||
return Buffer.from(dataUri.slice(base64Prefix.length), 'base64').toString('utf-8');
|
||||
}
|
||||
return decodeURIComponent(dataUri.replace(/^data:image\/svg\+xml,/, ''));
|
||||
}
|
||||
|
||||
|
|
@ -176,4 +181,26 @@ describe('sanitizeMcpIconPath', () => {
|
|||
it('returns an empty string for a malformed SVG data URI', () => {
|
||||
expect(sanitizeMcpIconPath('data:image/svg+xml')).toBe('');
|
||||
});
|
||||
|
||||
it('keeps the sanitized output within the schema length cap', () => {
|
||||
const raw = `<svg><text>${'A'.repeat(150_000)}</text></svg>`;
|
||||
const input = `data:image/svg+xml;base64,${Buffer.from(raw, 'utf-8').toString('base64')}`;
|
||||
expect(input.length).toBeLessThanOrEqual(MAX_MCP_ICON_PATH_LENGTH);
|
||||
const out = sanitizeMcpIconPath(input);
|
||||
expect(out.length).toBeLessThanOrEqual(MAX_MCP_ICON_PATH_LENGTH);
|
||||
expect(decode(out)).toContain('AAAA');
|
||||
});
|
||||
|
||||
it('never stores an icon over the length cap even when sanitizing grows it', () => {
|
||||
// A base64 input under the cap whose many self-closing tags expand under
|
||||
// sanitization (explicit close tags) past the cap; it must be dropped, not
|
||||
// stored over-limit where the next edit's re-validation would reject it.
|
||||
const cell = '<rect x="1" y="1" width="2" height="2" fill="#abc"/>';
|
||||
const raw = `<svg>${cell.repeat(3400)}</svg>`;
|
||||
const input = `data:image/svg+xml;base64,${Buffer.from(raw, 'utf-8').toString('base64')}`;
|
||||
expect(input.length).toBeLessThanOrEqual(MAX_MCP_ICON_PATH_LENGTH);
|
||||
const out = sanitizeMcpIconPath(input);
|
||||
expect(out.length).toBeLessThanOrEqual(MAX_MCP_ICON_PATH_LENGTH);
|
||||
expect(out).toBe('');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import sanitizeHtml from 'sanitize-html';
|
||||
import { MAX_MCP_ICON_PATH_LENGTH } from 'librechat-data-provider';
|
||||
|
||||
/**
|
||||
* Server-side sanitization for user-provided MCP server icons. The client
|
||||
|
|
@ -305,10 +306,17 @@ function normalizeIconValue(value: string): string {
|
|||
|
||||
/**
|
||||
* Sanitize a user-provided MCP `iconPath`. SVG data URIs are decoded, stripped
|
||||
* of active content via an allowlist, and re-encoded; a malformed SVG data URI
|
||||
* resolves to an empty string so a broken icon is stored rather than raw markup.
|
||||
* All other values (raster data URIs, URLs, relative paths) are returned
|
||||
* unchanged.
|
||||
* of active content via an allowlist, and re-encoded as base64; a malformed SVG
|
||||
* data URI resolves to an empty string so a broken icon is stored rather than
|
||||
* raw markup. All other values (raster data URIs, URLs, relative paths) are
|
||||
* returned unchanged.
|
||||
*
|
||||
* Base64 is far more compact than percent-encoding for the angle-bracket-heavy
|
||||
* SVG markup that sanitizing can even slightly grow (it expands self-closing
|
||||
* tags to explicit close tags), so it minimizes needless rejection. The result
|
||||
* is then measured against the schema length cap and dropped if it still exceeds
|
||||
* it, so a value that passed the cap on input can never be stored over it and
|
||||
* then rejected when the prefilled value is resubmitted on the next edit.
|
||||
*/
|
||||
export function sanitizeMcpIconPath(iconPath: string): string {
|
||||
const normalized = normalizeIconValue(iconPath);
|
||||
|
|
@ -320,5 +328,6 @@ export function sanitizeMcpIconPath(iconPath: string): string {
|
|||
return '';
|
||||
}
|
||||
const clean = scrubStyleBlocks(sanitizeHtml(svg, SVG_SANITIZE_OPTIONS));
|
||||
return `data:image/svg+xml,${encodeURIComponent(clean)}`;
|
||||
const encoded = `data:image/svg+xml;base64,${Buffer.from(clean, 'utf-8').toString('base64')}`;
|
||||
return encoded.length > MAX_MCP_ICON_PATH_LENGTH ? '' : encoded;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue