Compare commits

...

2 Commits

Author SHA1 Message Date
shamoon
6648941174 Chore: harden SafeUrlPipe 2025-12-18 00:08:21 -08:00
shamoon
7c8db78a62 Chore: use the MS playwright image for e2e testing in CI (#11607) 2025-12-16 08:46:12 -08:00
3 changed files with 53 additions and 22 deletions

View File

@@ -275,8 +275,12 @@ jobs:
tests-frontend-e2e:
name: "Frontend E2E Tests (Node ${{ matrix.node-version }} - ${{ matrix.shard-index }}/${{ matrix.shard-count }})"
runs-on: ubuntu-24.04
container: mcr.microsoft.com/playwright:v1.57.0-noble
needs:
- install-frontend-dependencies
env:
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
strategy:
fail-fast: false
matrix:
@@ -305,19 +309,8 @@ jobs:
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
- name: Re-link Angular cli
run: cd src-ui && pnpm link @angular/cli
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-playwright-
- name: Install Playwright system dependencies
run: npx playwright install-deps
- name: Install dependencies
run: cd src-ui && pnpm install --no-frozen-lockfile
- name: Install Playwright
run: cd src-ui && pnpm exec playwright install
- name: Run Playwright e2e tests
run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
frontend-bundle-analysis:

View File

@@ -13,20 +13,45 @@ describe('SafeUrlPipe', () => {
pipe = TestBed.inject(SafeUrlPipe)
})
it('should bypass security and trust the url', () => {
const url = 'https://example.com'
it('should trust only same-origin http/https urls', () => {
const origin = window.location.origin
const url = `${origin}/some/path`
const domSanitizer = TestBed.inject(DomSanitizer)
const sanitizerSpy = jest.spyOn(
domSanitizer,
'bypassSecurityTrustResourceUrl'
)
let safeResourceUrl = pipe.transform(url)
const safeResourceUrl = pipe.transform(url)
expect(safeResourceUrl).not.toBeNull()
expect(sanitizerSpy).toHaveBeenCalled()
expect(sanitizerSpy).toHaveBeenCalledWith(url)
})
safeResourceUrl = pipe.transform(null)
expect(safeResourceUrl).not.toBeNull()
expect(sanitizerSpy).toHaveBeenCalled()
it('should return null for null or unsafe urls', () => {
const sanitizerSpy = jest.spyOn(
TestBed.inject(DomSanitizer),
'bypassSecurityTrustResourceUrl'
)
expect(pipe.transform(null)).toBeTruthy()
expect(sanitizerSpy).toHaveBeenCalledWith('')
expect(pipe.transform('javascript:alert(1)')).toBeTruthy()
expect(sanitizerSpy).toHaveBeenCalledWith('')
const otherOrigin =
window.location.origin === 'https://example.com'
? 'https://evil.com'
: 'https://example.com'
expect(pipe.transform(`${otherOrigin}/file`)).toBeTruthy()
expect(sanitizerSpy).toHaveBeenCalledWith('')
})
it('should return null for malformed urls', () => {
const sanitizerSpy = jest.spyOn(
TestBed.inject(DomSanitizer),
'bypassSecurityTrustResourceUrl'
)
expect(pipe.transform('http://[invalid-url')).toBeTruthy()
expect(sanitizerSpy).toHaveBeenCalledWith('')
})
})

View File

@@ -1,5 +1,6 @@
import { Pipe, PipeTransform, inject } from '@angular/core'
import { DomSanitizer } from '@angular/platform-browser'
import { environment } from 'src/environments/environment'
@Pipe({
name: 'safeUrl',
@@ -7,11 +8,23 @@ import { DomSanitizer } from '@angular/platform-browser'
export class SafeUrlPipe implements PipeTransform {
private sanitizer = inject(DomSanitizer)
transform(url) {
if (url == null) {
transform(url: string | null) {
if (!url) return this.sanitizer.bypassSecurityTrustResourceUrl('')
try {
const parsed = new URL(url, window.location.origin)
const allowedOrigins = new Set([
window.location.origin,
new URL(environment.apiBaseUrl).origin,
])
const isHttp = ['http:', 'https:'].includes(parsed.protocol)
const originAllowed = allowedOrigins.has(parsed.origin)
if (!isHttp || !originAllowed) {
return this.sanitizer.bypassSecurityTrustResourceUrl('')
}
return this.sanitizer.bypassSecurityTrustResourceUrl(parsed.toString())
} catch {
return this.sanitizer.bypassSecurityTrustResourceUrl('')
} else {
return this.sanitizer.bypassSecurityTrustResourceUrl(url)
}
}
}