@@ -54,16 +54,29 @@ def redirect_if_historical_identifier(identifier_param, project)
5454 # Only redirect if:
5555 # 1. The parameter is a friendly_id slug (not numeric ID)
5656 # 2. The parameter doesn't match the project's current identifier
57- if param_value . friendly_id? && param_value != project . identifier
58- # Reconstruct the current URL with the new identifier
59- # Handle both path parameters (e.g., /workspaces/old-id) and query parameters (e.g., ?of=old-id)
60- new_url = request . url . sub (
61- /([?&]#{ identifier_param } =|\/ )(#{ Regexp . escape ( param_value ) } )(\b |&|$)/ ,
62- "\\ 1#{ project . identifier } \\ 3"
57+ if request . get? && param_value . friendly_id? && param_value != project . identifier
58+ # Reconstruct only the path and query string, not the full URL
59+ # This prevents Host header injection and open redirect attacks
60+ path = request . path
61+ query_string = request . query_string
62+
63+ # Replace the old identifier in the path
64+ new_path = path . sub (
65+ %r{(/)#{ Regexp . escape ( param_value ) } (/|$)} ,
66+ "\\ 1#{ project . identifier } \\ 2"
6367 )
6468
65- # Return 301 Moved Permanently
66- redirect new_url , permanent : true
69+ # Replace the old identifier in query parameters if present
70+ if query_string . present?
71+ new_query_string = query_string . gsub (
72+ /(\A |&)#{ Regexp . escape ( identifier_param . to_s ) } =#{ Regexp . escape ( param_value ) } (&|\z )/ ,
73+ "\\ 1#{ identifier_param } =#{ CGI . escape ( project . identifier ) } \\ 2"
74+ )
75+ new_path += "?#{ new_query_string } "
76+ end
77+
78+ # Return 301 Moved Permanently with path-only redirect
79+ redirect new_path , permanent : true
6780 end
6881 end
6982 end
0 commit comments