diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 4901cb3..4a5b173 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -5,6 +5,12 @@ on: schedule: - cron: '20 2/3 * * *' + +permissions: + issues: write + pull-requests: write + discussions: write + jobs: lock: runs-on: ubuntu-latest diff --git a/.github/workflows/pr-any.yml b/.github/workflows/pr-any.yml index 0ed99b0..f4d9983 100644 --- a/.github/workflows/pr-any.yml +++ b/.github/workflows/pr-any.yml @@ -1,6 +1,10 @@ name: PR on: [pull_request] + +permissions: + contents: read + jobs: pr: strategy: diff --git a/packages/extension-ui/src/util/sanitizeOrigin.spec.ts b/packages/extension-ui/src/util/sanitizeOrigin.spec.ts new file mode 100644 index 0000000..f13164a --- /dev/null +++ b/packages/extension-ui/src/util/sanitizeOrigin.spec.ts @@ -0,0 +1,35 @@ +// Copyright 2019-2026 @pezkuwi/extension-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type * as _ from '@pezkuwi/dev-test/globals.d.ts'; + +import { sanitizeOrigin, validateOrigin } from './sanitizeOrigin.js'; + +describe('sanitizeOrigin', () => { + it('keeps a normal origin as it is', () => { + expect(sanitizeOrigin('Pezkuwi Apps')).toEqual('Pezkuwi Apps'); + }); + + // One pass of tag removal turns `ipt>` into `ipt>alert(1)', + '<img src=x onerror=alert(1)>', + 'ay' + ])('leaves no angle bracket in %s', (origin) => { + const out = sanitizeOrigin(origin); + + expect(out).not.toMatch(/[<>]/); + }); + + // validateOrigin used to look for markup in the sanitized string, after the + // tags it was looking for had been removed: `x` passed as valid. + it.each(['ipt>evil', 'x'])('flags %s, which carried markup', (origin) => { + expect(validateOrigin(origin)).toBe(false); + }); + + it.each(['https://app.pezkuwichain.io', 'http://localhost:3000', 'Pezkuwi Apps'])('accepts %s', (origin) => { + expect(validateOrigin(origin)).toBe(true); + }); +}); diff --git a/packages/extension-ui/src/util/sanitizeOrigin.ts b/packages/extension-ui/src/util/sanitizeOrigin.ts index 2a5f7ff..c48e0c1 100644 --- a/packages/extension-ui/src/util/sanitizeOrigin.ts +++ b/packages/extension-ui/src/util/sanitizeOrigin.ts @@ -22,6 +22,11 @@ const sanitizeHtml = (input: string, options?: { .replace(/<[^>]*>/g, ''); // Remove other HTML tags } + // Tag removal is a single pass, so nested input such as `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() @@ -44,6 +49,11 @@ export const sanitizeOrigin = (origin: string): string => { // 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 @@ -53,7 +63,6 @@ export const validateOrigin = (origin: string): boolean => { // Check for suspicious patterns const suspiciousPatterns = [ - /[<>]/, // HTML characters /^\s*$/, // Empty or whitespace only /.{50,}/, // Extremely long names /^(?!https?:\/\/).*:\/\//, // Any protocol other than http/https