@@ -29,13 +29,45 @@ const jwtSchema = z.object({
2929 channel_id : z . number ( ) . nullable ( ) ,
3030} ) ;
3131
32- export async function GET ( request : NextRequest ) {
33- function appendExchangeToken ( url : string , token : string ) : string {
34- const delimiter = new URL ( url , env . APP_ORIGIN ) . search ? '&' : '?' ;
32+ /**
33+ * It can be provided a URL where `product_name` contains raw reserved characters
34+ * (e.g. "Widget #2", "AT&T", "plus+sign"). These can be interpreted as URL delimiters
35+ * (`#` fragment, `&` query separator, `+` becomes space in x-www-form-urlencoded parsing),
36+ * truncating or mutating the query param value.
37+ *
38+ * We sanitize the value in-place so Next can receive the full name.
39+ */
40+ function sanitizeQueryParamValue ( value : string ) : string {
41+ const withSafePercents = value . replace ( / % (? ! [ 0 - 9 A - F a - f ] { 2 } ) / g, '%25' ) ;
3542
36- return `${ url } ${ delimiter } exchangeToken=${ token } ` ;
37- }
43+ return withSafePercents
44+ . replaceAll ( '+' , '%2B' )
45+ . replaceAll ( '#' , '%23' )
46+ . replaceAll ( '&' , '%26' )
47+ . replaceAll ( ' ' , '%20' ) ;
48+ }
49+
50+ function sanitizeQueryParamValueInPath ( path : string , paramName : string ) : string {
51+ const key = `${ paramName } =` ;
52+ const keyIdx = path . indexOf ( key ) ;
53+ if ( keyIdx === - 1 ) return path ;
54+
55+ const valueStart = keyIdx + key . length ;
56+ const remainder = path . slice ( valueStart ) ;
57+ const delimiterMatch = remainder . match ( / & [ A - Z a - z 0 - 9 _ . ~ - ] + = / ) ;
58+ const endIdx =
59+ delimiterMatch && typeof delimiterMatch . index === 'number'
60+ ? valueStart + delimiterMatch . index
61+ : path . length ;
3862
63+ const before = path . slice ( 0 , valueStart ) ;
64+ const value = path . slice ( valueStart , endIdx ) ;
65+ const after = path . slice ( endIdx ) ;
66+
67+ return `${ before } ${ sanitizeQueryParamValue ( value ) } ${ after } ` ;
68+ }
69+
70+ export async function GET ( request : NextRequest ) {
3971 const parsedParams = queryParamSchema . safeParse (
4072 Object . fromEntries ( request . nextUrl . searchParams )
4173 ) ;
@@ -64,8 +96,14 @@ export async function GET(request: NextRequest) {
6496 } ) ;
6597
6698 const exchangeToken = await db . saveClientToken ( clientToken ) ;
99+ const safePath = sanitizeQueryParamValueInPath (
100+ sanitizeQueryParamValueInPath ( path , 'product_name' ) ,
101+ 'productName'
102+ ) ;
103+ const redirectUrl = new URL ( safePath , env . APP_ORIGIN ) ;
104+ redirectUrl . searchParams . set ( 'exchangeToken' , exchangeToken ) ;
67105
68- return NextResponse . redirect ( new URL ( appendExchangeToken ( path , exchangeToken ) , env . APP_ORIGIN ) , {
106+ return NextResponse . redirect ( redirectUrl , {
69107 status : 302 ,
70108 statusText : 'Found' ,
71109 } ) ;
0 commit comments