Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/utils/__tests__/native-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,17 @@ describe('redactNativePath (deep-link telemetry)', () => {
expect(redactNativePath('/not-a-declared-route/secret-value')).toBe('/:id/:id')
})

// The API allows usernames such as `bank` or `crypto`, and `/<username>` is
// a profile link — a safe sub-view token must not leak one from the
// identifier position.
it('keeps safe sub-view tokens only in the sub-view position', () => {
expect(redactNativePath('https://peanut.me/bank')).toBe('https://peanut.me/:id')
expect(redactNativePath('/profile/crypto')).toBe('/profile/:id')
expect(redactNativePath('/manteca/success')).toBe('/:id/:id')
expect(redactNativePath('/add-money/us/bank')).toBe('/add-money/:id/bank')
expect(redactNativePath('/withdraw/manteca')).toBe('/withdraw/:id')
})

// The authority can carry userinfo, which is attacker-controlled on a link
// and would otherwise survive into `raw` next to the redacted path.
it('drops userinfo from the authority', () => {
Expand Down
15 changes: 11 additions & 4 deletions src/utils/native-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,10 @@ function baseOrigin(): string | null {
/**
* Static sub-view segments that carry diagnostic value and no identifier.
* Everything NOT here and not a route root is treated as an identifier.
*
* Honoured only in the sub-view position — `/<root>/<id>/<sub-view>` — so a
* user whose username collides with one of these (`/bank` is a valid profile
* link) is still redacted at the identifier position.
*/
const TELEMETRY_SAFE_SEGMENTS = new Set(['success', 'bank', 'manteca', 'crypto', 'us'])

Expand Down Expand Up @@ -376,11 +380,14 @@ export function redactNativePath(value: string): string {
const authority = beforeQuery.match(/^[a-z][a-z0-9+.-]*:\/\/[^/]*/i)?.[0] ?? ''
const prefix = authority.replace(/\/\/[^/]*@/, '//')
const path = beforeQuery.slice(authority.length)
const redacted = path
.split('/')
.map((segment) => {
const segments = path.split('/')
const redacted = segments
.map((segment, i) => {
if (segment === '') return segment
if (NATIVE_EXPORT_ROOTS.has(segment) || TELEMETRY_SAFE_SEGMENTS.has(segment)) return segment
if (NATIVE_EXPORT_ROOTS.has(segment)) return segment
// sub-view position only: two after a root, `/qr/<code>/success`
const underRoot = i >= 2 && NATIVE_EXPORT_ROOTS.has(segments[i - 2])
if (underRoot && TELEMETRY_SAFE_SEGMENTS.has(segment)) return segment
return ':id'
})
.join('/')
Expand Down
Loading