Files
KArtSell.Aegis/docs/Design/kbx-foundation-v60-status-canonical-contract-hardening/packages/kbx-ui/src/shell/navigationSecurity.ts
T
kjh2064 3f293d8aa8
deploy / deploy (push) Successful in 1m52s
deploy / notify (push) Successful in 1s
V13-FE-006: consolidate approved UI and contract hardening
2026-08-13 02:41:00 +09:00

48 lines
2.7 KiB
TypeScript

import type { KbxNavigationResolvedEntry } from '@kbx/contracts'
const MAX_WORKSPACE_URL_LENGTH=2048
function normalizePathname(value:string){const clean=value.replace(/\/+$/,'')||'/';return clean.startsWith('/')?clean:`/${clean}`}
function hasUnsafeEncodedPath(value:string){return value.length>MAX_WORKSPACE_URL_LENGTH || /[\u0000-\u001f\u007f]|\\|%2f|%5c|%00|%0d|%0a|%09|%2e%2e/i.test(value)}
/** Match a Vue-Router-style path pattern without trusting persisted route strings. */
export function matchKbxRoutePattern(pattern:string,candidatePath:string){
if(hasUnsafeEncodedPath(candidatePath))return false
const p=normalizePathname(pattern).split('/').filter(Boolean)
const c=normalizePathname(candidatePath).split('/').filter(Boolean)
if(p.length!==c.length)return false
return p.every((segment,index)=>segment.startsWith(':') ? Boolean(c[index]) : segment===c[index])
}
/**
* Re-resolve an in-memory workspace path against the screen's declared route capabilities.
* Query/hash are retained only for the current in-memory workspace after the pathname passes the capability check.
*/
export function resolveKbxSafeWorkspacePath(patterns:readonly string[],candidatePath:string,fallbackPath:string){
try{
if(hasUnsafeEncodedPath(candidatePath))return fallbackPath
const candidate=new URL(candidatePath,'https://kbx.local')
if(candidate.origin!=='https://kbx.local'||candidate.username||candidate.password)return fallbackPath
const allowed=patterns.some(pattern=>matchKbxRoutePattern(pattern,candidate.pathname))
return allowed?`${candidate.pathname}${candidate.search}${candidate.hash}`:fallbackPath
}catch{return fallbackPath}
}
/**
* Re-resolve untrusted persisted recent navigation against the current authorized catalog entry.
* Query/hash are deliberately stripped before persistence/replay so transient filters, tokens, and sensitive
* values do not leak into local preference storage. Dynamic route path persistence remains explicit opt-in.
*/
export function resolveKbxSafeRecentPath(entry:KbxNavigationResolvedEntry, storedPath:string){
if(entry.recentPolicy!=='route')return entry.path
try{
if(hasUnsafeEncodedPath(storedPath))return entry.path
const base=new URL(entry.path,'https://kbx.local')
const candidate=new URL(storedPath,'https://kbx.local')
if(candidate.origin!==base.origin||candidate.username||candidate.password)return entry.path
const basePath=base.pathname.replace(/\/$/,'')||'/'
const candidatePath=candidate.pathname.replace(/\/$/,'')||'/'
const within=candidatePath===basePath || (basePath!=='/' && candidatePath.startsWith(`${basePath}/`))
return within?candidate.pathname:entry.path
}catch{return entry.path}
}