From c169721ebfee08a8bca57ff16db90dc30ca8e10d Mon Sep 17 00:00:00 2001 From: click33 <2393584716@qq.com> Date: Thu, 9 May 2024 11:04:50 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A6=81=E6=AD=A2=20allow-url=20=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E9=A1=B9=20*=20=E7=AC=A6=E5=8F=B7=E5=87=BA=E7=8E=B0?= =?UTF-8?q?=E5=9C=A8=E4=B8=AD=E9=97=B4=E4=BD=8D=E7=BD=AE=EF=BC=8C=E5=9B=A0?= =?UTF-8?q?=E4=B8=BA=E8=BF=99=E6=9C=89=E5=8F=AF=E8=83=BD=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E8=A2=AB=E7=BB=95=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../satoken/sso/config/SaSsoServerConfig.java | 9 +- .../satoken/sso/error/SaSsoErrorCode.java | 3 + .../sso/template/SaSsoServerTemplate.java | 82 +++++++++++++++---- 3 files changed, 77 insertions(+), 17 deletions(-) diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/config/SaSsoServerConfig.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/config/SaSsoServerConfig.java index 47aa280f..4249fb6a 100644 --- a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/config/SaSsoServerConfig.java +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/config/SaSsoServerConfig.java @@ -22,10 +22,12 @@ import cn.dev33.satoken.sso.function.CheckTicketAppendDataFunction; import cn.dev33.satoken.sso.function.DoLoginHandleFunction; import cn.dev33.satoken.sso.function.NotLoginViewFunction; import cn.dev33.satoken.sso.function.SendHttpFunction; +import cn.dev33.satoken.sso.template.SaSsoServerTemplate; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; import java.io.Serializable; +import java.util.List; /** * Sa-Token SSO 单点登录模块 配置类 (Server端) @@ -134,6 +136,11 @@ public class SaSsoServerConfig implements Serializable { * @return 对象自身 */ public SaSsoServerConfig setAllowUrl(String allowUrl) { + // 提前校验一下配置的 allowUrl 是否合法,让开发者尽早发现错误 + if(SaFoxUtil.isNotEmpty(allowUrl)) { + List allowUrlList = SaFoxUtil.convertStringToList(allowUrl); + SaSsoServerTemplate.checkAllowUrlListStaticMethod(allowUrlList); + } this.allowUrl = allowUrl; return this; } @@ -243,7 +250,7 @@ public class SaSsoServerConfig implements Serializable { * @return 对象自身 */ public SaSsoServerConfig setAllow(String ...url) { - this.allowUrl = SaFoxUtil.arrayJoin(url); + this.setAllowUrl(SaFoxUtil.arrayJoin(url)); return this; } diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/error/SaSsoErrorCode.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/error/SaSsoErrorCode.java index 9d0c6b16..49b67781 100644 --- a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/error/SaSsoErrorCode.java +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/error/SaSsoErrorCode.java @@ -65,4 +65,7 @@ public interface SaSsoErrorCode { /** 在 /sso/auth 既没有指定 redirect 参数,也没有配置 homeRoute 路由 */ int CODE_30014 = 30014; + /** 无效的 allow-url 配置 */ + int CODE_30015 = 30015; + } diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoServerTemplate.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoServerTemplate.java index b8748d84..d5367e17 100644 --- a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoServerTemplate.java +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoServerTemplate.java @@ -264,32 +264,82 @@ public class SaSsoServerTemplate extends SaSsoTemplate { } // 3、不允许出现@字符 - // 为什么不允许出现 @ 字符呢,因为这有可能导致 redirect 参数绕过 AllowUrl 列表的校验 - // 举个例子 配置文件: - // sa-token.sso-server.allow-url=http://sa-sso-client1.com* - // 开发者原意是为了允许 sa-sso-client1.com 下的所有地址都可以下放ticket - // 但是如果攻击者精心构建一个url: - // http://sa-sso-server.com:9000/sso/auth?redirect=http://sa-sso-client1.com@sa-token.cc - // 那么这个url就会绕过 allow-url 的校验,ticket 被下发到了第三方服务器地址: - // https://sa-token.cc/?ticket=i8vDfbpqBViMe01QoLY1kHROJWYvv9plBtvTZ6kk77KK0e0U4Xj99NPfSZEYjRul - // 造成了ticket 参数劫持 - // 所以此处需要禁止在 url 中出现 @ 字符 - // 这么一刀切的做法,可能会导致一些特殊的正常url也无法通过校验,例如: - // http://sa-sso-server.com:9000/sso/auth?redirect=http://sa-sso-client1.com:9003/@getInfo - // 但是为了安全起见,这么做还是有必要的 if(url.contains("@")) { + // 为什么不允许出现 @ 字符呢,因为这有可能导致 redirect 参数绕过 AllowUrl 列表的校验 + // + // 举个例子 配置文件: + // sa-token.sso-server.allow-url=http://sa-sso-client1.com* + // + // 开发者原意是为了允许 sa-sso-client1.com 下的所有地址都可以下放ticket + // + // 但是如果攻击者精心构建一个url: + // http://sa-sso-server.com:9000/sso/auth?redirect=http://sa-sso-client1.com@sa-token.cc + // + // 那么这个url就会绕过 allow-url 的校验,ticket 被下发到了第三方服务器地址: + // https://sa-token.cc/?ticket=i8vDfbpqBViMe01QoLY1kHROJWYvv9plBtvTZ6kk77KK0e0U4Xj99NPfSZEYjRul + // + // 造成了ticket 参数劫持 + // 所以此处需要禁止在 url 中出现 @ 字符 + // + // 这么一刀切的做法,可能会导致一些特殊的正常url也无法通过校验,例如: + // http://sa-sso-server.com:9000/sso/auth?redirect=http://sa-sso-client1.com:9003/@getInfo + // + // 但是为了安全起见,这么做还是有必要的 throw new SaSsoException("无效redirect(不允许出现@字符):" + url).setCode(SaSsoErrorCode.CODE_30001); } - // 4、判断是否在[允许地址列表]之中 - List authUrlList = Arrays.asList(getAllowUrl().replaceAll(" ", "").split(",")); - if( ! SaStrategy.instance.hasElement.apply(authUrlList, url) ) { + // 4、判断是否在 [ 允许的地址列表 ] 之中 + List allowUrlList = Arrays.asList(getAllowUrl().replaceAll(" ", "").split(",")); + checkAllowUrlList(allowUrlList); + if( ! SaStrategy.instance.hasElement.apply(allowUrlList, url) ) { throw new SaSsoException("非法redirect:" + url).setCode(SaSsoErrorCode.CODE_30002); } // 校验通过 √ } + /** + * 校验配置的 AllowUrl 是否合规,如果不合规则抛出异常 + * @param allowUrlList 待校验的 allow-url 地址列表 + */ + public void checkAllowUrlList(List allowUrlList){ + checkAllowUrlListStaticMethod(allowUrlList); + } + + /** + * 校验配置的 AllowUrl 是否合规,如果不合规则抛出异常 + * @param allowUrlList 待校验的 allow-url 地址列表 + */ + public static void checkAllowUrlListStaticMethod(List allowUrlList){ + for (String url : allowUrlList) { + int index = url.indexOf("*"); + // 如果配置了 * 字符,则必须出现在最后一位,否则属于无效配置项 + if(index != -1 && index != url.length() - 1) { + // 为什么不允许 * 字符出现在中间位置呢,因为这有可能导致 redirect 参数绕过 allow-url 列表的校验 + // + // 举个例子 配置文件: + // sa-token.sso-server.allow-url=http://*.sa-sso-client1.com + // + // 开发者原意是为了允许 sa-sso-client1.com 下的所有子域名都可以下放ticket + // 例如:http://shop.sa-sso-client1.com + // + // 但是如果攻击者精心构建一个url: + // http://sa-sso-server.com:9000/sso/auth?redirect=http://sa-token.cc/a.sa-sso-client1.com/sso/login + // + // 那么这个 url 就会绕过 allow-url 的校验,ticket 被下发到了第三方服务器地址: + // https://sa-token.cc/a.sa-sso-client1.com/sso/login?ticket=v2KKMUFK7dDsMMzXLQ3aWGsyGUjrA0dBB2jeOWrpCnC8b5ScmXXQSv20mIwPK7Cx + // + // 造成了 ticket 参数劫持 + // 所以此处需要禁止 allow-url 配置项的中间位置出现 * 字符(出现在末尾是没有问题的) + // + // 这么一刀切的做法,可能会导致正常场景下的子域名url也无法通过校验,例如: + // http://sa-sso-server.com:9000/sso/auth?redirect=http://shop.sa-sso-client1.com/sso/login + // + // 但是为了安全起见,这么做还是有必要的 + throw new SaSsoException("无效的 allow-url 配置(*通配符只允许出现在最后一位):" + url).setCode(SaSsoErrorCode.CODE_30015); + } + } + } // ------------------- SSO -------------------