mirror of
https://github.com/pezkuwichain/pezkuwi-extension.git
synced 2026-11-02 08:01:06 +00:00
* fix(ui): leave no angle bracket in a sanitized origin; judge markup on the raw one sanitizeOrigin removed tags in a single pass, so `<scr<script>ipt>` came out as `<script` (CodeQL js/incomplete-multi-character-sanitization #6, #7). The page renders the result as React text, so this was not exploitable here, but the helper's name promised more than it did. It now also drops every remaining `<` and `>`. validateOrigin looked for markup in the sanitized string, after the tags had been removed, so `<b>x</b>` passed as a valid origin. It now checks the raw origin first. pr-any.yml gets permissions: contents: read and lock.yml the issues/pull-requests/discussions write it needs (CodeQL actions/missing-workflow-permissions #3, #23). * test(ui): typed cases and the matchers the test shim implements The first version used it.each (untyped rows, @typescript-eslint/no-unsafe-argument) and expect().not.toMatch, which @pezkuwi/dev-test does not implement. Plain loops over string[] and a boolean assertion instead. With the repo runner: 5/5 pass on the fix, 3 of 5 fail on main's sanitizeOrigin.ts.
74 lines
2.2 KiB
TypeScript
74 lines
2.2 KiB
TypeScript
// Copyright 2019-2026 @pezkuwi/extension-ui authors & contributors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
const sanitizeHtml = (input: string, options?: {
|
|
allowLineBreaks?: boolean;
|
|
maxLength?: number;
|
|
}): string => {
|
|
if (!input || typeof input !== 'string') {
|
|
return '';
|
|
}
|
|
|
|
let sanitized = input;
|
|
|
|
// Remove all HTML tags
|
|
sanitized = sanitized.replace(/<[^>]*>/g, '');
|
|
|
|
// Handle line breaks if specified
|
|
if (options?.allowLineBreaks) {
|
|
// Convert <br> to actual line breaks before sanitization
|
|
sanitized = input
|
|
.replace(/<br\s*\/?>/gi, '\n')
|
|
.replace(/<[^>]*>/g, ''); // Remove other HTML tags
|
|
}
|
|
|
|
// Tag removal is a single pass, so nested input such as `<scr<script>ipt>`
|
|
// leaves a new tag behind. Drop every remaining angle bracket; an origin never
|
|
// contains one, and validateOrigin already treats them as suspicious.
|
|
sanitized = sanitized.replace(/[<>]/g, '');
|
|
|
|
// Normalize whitespace
|
|
sanitized = sanitized
|
|
.trim()
|
|
.replace(/\s+/g, ' ');
|
|
|
|
// Apply length limit if specified
|
|
if (options?.maxLength && sanitized.length > options.maxLength) {
|
|
sanitized = sanitized.substring(0, options.maxLength) + '...';
|
|
}
|
|
|
|
return sanitized;
|
|
};
|
|
|
|
export const sanitizeOrigin = (origin: string): string => {
|
|
return sanitizeHtml(origin, {
|
|
allowLineBreaks: false,
|
|
maxLength: 50
|
|
});
|
|
};
|
|
|
|
// Validate that the origin looks legitimate
|
|
export const validateOrigin = (origin: string): boolean => {
|
|
// Markup is judged on what the site sent: the sanitized form no longer has any.
|
|
if (/[<>]/.test(origin)) {
|
|
return false;
|
|
}
|
|
|
|
const sanitized = sanitizeOrigin(origin);
|
|
|
|
// Special case: always allow localhost URLs
|
|
if (/^https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|::1)(?::\d+)?(?:\/|$)/.test(sanitized)) {
|
|
return true;
|
|
}
|
|
|
|
// Check for suspicious patterns
|
|
const suspiciousPatterns = [
|
|
/^\s*$/, // Empty or whitespace only
|
|
/.{50,}/, // Extremely long names
|
|
/^(?!https?:\/\/).*:\/\//, // Any protocol other than http/https
|
|
/^https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?::\d+)?(?:\/|$)/ // Raw IP addresses (except localhost)
|
|
];
|
|
|
|
return !suspiciousPatterns.some((pattern) => pattern.test(sanitized));
|
|
};
|