diff --git a/backend/internal/handler/dev_handler.go b/backend/internal/handler/dev_handler.go index 0b32a39a..e64e2e61 100644 --- a/backend/internal/handler/dev_handler.go +++ b/backend/internal/handler/dev_handler.go @@ -13,6 +13,7 @@ import ( "fmt" "io" "log/slog" + "net" "net/http" "net/url" "os" @@ -2749,16 +2750,35 @@ func validateBackchannelLogoutURI(raw string) error { case "https": return nil case "http": - host := strings.ToLower(parsed.Hostname()) - if host == "localhost" || host == "127.0.0.1" { + if isAllowedLocalBackchannelLogoutHost(parsed.Hostname()) { return nil } - return fmt.Errorf("backchannelLogoutUri must use https outside localhost development") + return fmt.Errorf("backchannelLogoutUri must use https outside local development") default: return fmt.Errorf("backchannelLogoutUri must use http or https") } } +func isAllowedLocalBackchannelLogoutHost(rawHost string) bool { + host := strings.ToLower(strings.TrimSpace(rawHost)) + if host == "" { + return false + } + + switch host { + case "localhost", "127.0.0.1", "::1", "host.docker.internal": + return true + } + + if ip := net.ParseIP(host); ip != nil { + return ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() + } + + // Docker service names and other single-label local hostnames are + // permitted only for local HTTP development workflows. + return !strings.Contains(host, ".") +} + func normalizeClientAutoLoginMetadata(metadata map[string]interface{}) (map[string]interface{}, error) { if metadata == nil { return metadata, nil diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx index b189abdd..8e3ace74 100644 --- a/devfront/src/features/clients/ClientGeneralPage.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.tsx @@ -288,7 +288,26 @@ function isValidBackchannelLogoutUrl(value: string): boolean { if (url.protocol !== "http:") { return false; } - return url.hostname === "localhost" || url.hostname === "127.0.0.1"; + const host = url.hostname.toLowerCase(); + if ( + host === "localhost" || + host === "127.0.0.1" || + host === "::1" || + host === "host.docker.internal" + ) { + return true; + } + if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) { + return ( + host.startsWith("10.") || + host.startsWith("192.168.") || + /^172\.(1[6-9]|2\d|3[0-1])\./.test(host) || + host.startsWith("169.254.") + ); + } + // Docker service names and other single-label local hosts are allowed + // only for HTTP local development use. + return !host.includes("."); } catch { return false; } @@ -949,7 +968,7 @@ function ClientGeneralPage() { throw new Error( t( "msg.dev.clients.general.backchannel_logout.invalid", - "Back-Channel Logout URI 형식이 올바르지 않습니다. 운영 환경은 https, 로컬 개발 환경은 localhost/127.0.0.1의 http만 허용됩니다.", + "Back-Channel Logout URI 형식이 올바르지 않습니다. 운영 환경은 https를 사용하고, 로컬 개발 환경은 localhost/127.0.0.1, host.docker.internal, Docker 서비스명, 사설 IP의 http만 허용됩니다.", ), ); } @@ -1590,7 +1609,7 @@ function ClientGeneralPage() {
{t( "msg.dev.clients.general.backchannel_logout.invalid", - "Back-Channel Logout URI 형식이 올바르지 않습니다. 운영 환경은 https, 로컬 개발 환경은 localhost/127.0.0.1의 http만 허용됩니다.", + "Back-Channel Logout URI 형식이 올바르지 않습니다. 운영 환경은 https를 사용하고, 로컬 개발 환경은 localhost/127.0.0.1, host.docker.internal, Docker 서비스명, 사설 IP의 http만 허용됩니다.", )}
) : null} diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml index a4d69010..f7daf9d7 100644 --- a/devfront/src/locales/en.toml +++ b/devfront/src/locales/en.toml @@ -435,7 +435,7 @@ help = "Enter the redirect URIs. You can modify them in the Federation tab after [msg.dev.clients.general.backchannel_logout] uri_help = "RP endpoint that receives Baron's session termination event via server-to-server POST." -invalid = "The Back-Channel Logout URI format is invalid. Production requires https, and local development only allows http on localhost/127.0.0.1." +invalid = "The Back-Channel Logout URI format is invalid. Production requires https, and local development only allows http on localhost/127.0.0.1, host.docker.internal, Docker service names, and private IPs." session_required_help = "Use this when the RP should process logout_token only if the sid claim is included." session_required_on = "On: process logout only when the logout_token contains a sid." session_required_off = "Off: process logout using sub even if sid is missing." diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml index eecb25aa..678f9a7d 100644 --- a/devfront/src/locales/ko.toml +++ b/devfront/src/locales/ko.toml @@ -435,7 +435,7 @@ help = "인증 후 리다이렉트될 URI를 입력하세요. 생성 후 연동 [msg.dev.clients.general.backchannel_logout] uri_help = "Baron이 세션 종료 이벤트를 서버 간 POST로 전달할 RP endpoint입니다." -invalid = "Back-Channel Logout URI 형식이 올바르지 않습니다. 운영 환경은 https, 로컬 개발 환경은 localhost/127.0.0.1의 http만 허용됩니다." +invalid = "Back-Channel Logout URI 형식이 올바르지 않습니다. 운영 환경은 https를 사용하고, 로컬 개발 환경은 localhost/127.0.0.1, host.docker.internal, Docker 서비스명, 사설 IP의 http만 허용됩니다." session_required_help = "RP가 logout_token에 sid claim이 포함된 경우에만 처리하도록 요구할 때 사용합니다." session_required_on = "켜면: logout_token 안에 sid가 있을 때만 로그아웃 처리" session_required_off = "끄면: sid가 없어도 sub만으로 로그아웃 처리 가능"