实际上用户@Yaneeve 提出了一个不错的解决方案,但它有一些缺点,例如它
- 仅适用于
call(),不适用于execution(),
- 需要反思,
- 需要
declare precedence,
- 需要事先知道 hack 的类和包名(好的,这可以通过在优先声明中使用
* 来规避)。
我有一个更稳定的解决方案给你。我已将源代码修改为更真实一点:
验证者:
身份验证器有一个用户数据库(为简单起见硬编码),并且实际上比较用户和密码。
package de.scrum_master.app;
import java.util.HashMap;
import java.util.Map;
public class Authenticator {
private static final Map<String, String> userDB = new HashMap<>();
static {
userDB.put("alice", "aaa");
userDB.put("bob", "bbb");
userDB.put("dave", "ddd");
userDB.put("erin", "eee");
}
public boolean authenticate(String user, String pass) {
return userDB.containsKey(user) && userDB.get(user).equals(pass);
}
}
应用:
应用程序有一个入口点并尝试验证一些用户,并打印结果:
package de.scrum_master.app;
public class Application {
public static void main(String[] args) {
Authenticator authenticator = new Authenticator();
System.out.println("Status: " + authenticator.authenticate("alice", "aaa"));
System.out.println("Status: " + authenticator.authenticate("bob", "xxx"));
System.out.println("Status: " + authenticator.authenticate("dave", "ddd"));
System.out.println("Status: " + authenticator.authenticate("erin", "xxx"));
System.out.println("Status: " + authenticator.authenticate("hacker", "xxx"));
}
}
应用程序的输出如下:
Status: true
Status: false
Status: true
Status: false
Status: false
身份验证记录器方面:
我想添加一个法律方面,其中包含有关身份验证方法的around() 建议,就像稍后的黑客方面一样。
package de.scrum_master.aspect;
import de.scrum_master.app.Authenticator;
public aspect AuthenticationLogger {
pointcut authentication(String user) :
execution(boolean Authenticator.authenticate(String, String)) && args(user, *);
boolean around(String user): authentication(user) {
boolean result = proceed(user);
System.out.println("[INFO] Authentication result for '" + user + "' = " + result);
return result;
}
}
输出变成:
[INFO] Authentication result for 'alice' = true
Status: true
[INFO] Authentication result for 'bob' = false
Status: false
[INFO] Authentication result for 'dave' = true
Status: true
[INFO] Authentication result for 'erin' = false
Status: false
[INFO] Authentication result for 'hacker' = false
Status: false
如您所见,只要系统没有被黑客入侵,“状态”和“认证结果”是相同的。这并不奇怪。
黑客方面:
现在让我们破解系统。我们可以始终返回 true(肯定的身份验证结果)或始终为某个用户返回 true - 无论我们喜欢什么。如果我们想要它的副作用,我们甚至可以 proceed() 到原来的调用,但我们仍然可以总是返回 true,这就是我们在这个例子中所做的:
package de.scrum_master.hack;
import de.scrum_master.app.Authenticator;
public aspect Hack {
declare precedence : *, Hack;
pointcut authentication() :
execution(boolean Authenticator.authenticate(String, String));
boolean around(): authentication() {
System.out.println("Hack is active!");
proceed();
return true;
}
}
输出变为:
Hack is active!
[INFO] Authentication result for 'alice' = true
Status: true
Hack is active!
[INFO] Authentication result for 'bob' = true
Status: true
Hack is active!
[INFO] Authentication result for 'dave' = true
Status: true
Hack is active!
[INFO] Authentication result for 'erin' = true
Status: true
Hack is active!
[INFO] Authentication result for 'hacker' = true
Status: true
因为黑客方面声明自己是通知优先级中的最后一个(即在同一连接点上嵌套的一系列proceed() 调用中的最内层外壳,其返回值将沿调用链向上传播到记录器方面,这就是为什么记录器在从内部接收到它之后打印已经操纵的身份验证结果。
如果我们将声明更改为declare precedence : Hack, *;,输出如下:
Hack is active!
[INFO] Authentication result for 'alice' = true
Status: true
Hack is active!
[INFO] Authentication result for 'bob' = false
Status: true
Hack is active!
[INFO] Authentication result for 'dave' = true
Status: true
Hack is active!
[INFO] Authentication result for 'erin' = false
Status: true
Hack is active!
[INFO] Authentication result for 'hacker' = false
Status: true
即记录器现在记录原始结果并将其沿调用链传播到黑客方面,黑客方面可以在最后操纵它,因为它是优先的,因此可以控制整个调用链。拥有最终决定权是黑客通常想要的,但在这种情况下,它会显示记录的内容(一些身份验证为真,一些为假)与应用程序的实际行为方式(始终为真,因为它被黑客入侵)之间的不匹配。
反黑客方面:
现在,最后但并非最不重要的一点是,我们要拦截建议执行并确定它们是否来自可能的黑客方面。好消息是:AspectJ 有一个切入点,称为adviceexecution() - nomen est omen。 :-)
建议执行连接点具有可通过thisJoinPoint.getArgs() 确定的参数。不幸的是,AspectJ 无法通过args() 将它们绑定到参数。如果截获的通知是around() 类型,则第一个adviceexecution() 参数将是AroundClosure 对象。如果在这个闭包对象上调用run() 方法并指定正确的参数(可以通过getState() 确定),效果是不会执行实际的通知主体,而只会隐式调用proceed()。这有效地禁用了拦截的建议!
package de.scrum_master.aspect;
import org.aspectj.lang.SoftException;
import org.aspectj.runtime.internal.AroundClosure;
public aspect AntiHack {
pointcut catchHack() :
adviceexecution() && ! within(AntiHack) && !within(AuthenticationLogger);
Object around() : catchHack() {
Object[] adviceArgs = thisJoinPoint.getArgs();
if (adviceArgs[0] instanceof AroundClosure) {
AroundClosure aroundClosure = (AroundClosure) adviceArgs[0];
Object[] closureState = aroundClosure.getState();
System.out.println("[WARN] Disabling probable authentication hack: " + thisJoinPointStaticPart);
try {
return aroundClosure.run(closureState);
} catch (Throwable t) {
throw new SoftException(t);
}
}
return proceed();
}
}
结果输出是:
[WARN] Disabling probable authentication hack: adviceexecution(boolean de.scrum_master.hack.Hack.around(AroundClosure))
[INFO] Authentication result for 'alice' = true
Status: true
[WARN] Disabling probable authentication hack: adviceexecution(boolean de.scrum_master.hack.Hack.around(AroundClosure))
[INFO] Authentication result for 'bob' = false
Status: false
[WARN] Disabling probable authentication hack: adviceexecution(boolean de.scrum_master.hack.Hack.around(AroundClosure))
[INFO] Authentication result for 'dave' = true
Status: true
[WARN] Disabling probable authentication hack: adviceexecution(boolean de.scrum_master.hack.Hack.around(AroundClosure))
[INFO] Authentication result for 'erin' = false
Status: false
[WARN] Disabling probable authentication hack: adviceexecution(boolean de.scrum_master.hack.Hack.around(AroundClosure))
[INFO] Authentication result for 'hacker' = false
Status: false
如你所见,
- 现在的结果与没有黑客方面的结果相同,即我们有效地禁用了它,
- 没有必要知道黑客方面的类或包名,但是在我们的
catchHack() 切入点中,我们指定了一个已知方面的白名单不应禁用,即保持不变,
- 我们只针对
around() 建议,因为before() 和after() 建议的签名没有AroundClosures。
目标方法启发式反黑客建议:
不幸的是,我发现无法确定周围闭包所针对的方法,因此没有确切的方法将反黑客建议的范围限制为专门针对我们想要防止黑客攻击的方法的建议。在这个例子中,我们可以通过启发式检查AroundClosure.getState()返回的数组的内容来缩小范围,该数组包含
- advice的目标对象作为第一个参数(我们需要检查它是否是
Authenticator实例),
- 目标方法调用的参数(对于
Authenticator.authenticate(),必须有两个Strings)。
这些知识是无证的(就像建议执行的参数的内容一样),我是通过反复试验发现的。无论如何,此修改启用了启发式:
package de.scrum_master.aspect;
import org.aspectj.lang.SoftException;
import org.aspectj.runtime.internal.AroundClosure;
import de.scrum_master.app.Authenticator;
public aspect AntiHack {
pointcut catchHack() :
adviceexecution() && ! within(AntiHack) && !within(AuthenticationLogger);
Object around() : catchHack() {
Object[] adviceArgs = thisJoinPoint.getArgs();
if (adviceArgs[0] instanceof AroundClosure) {
AroundClosure aroundClosure = (AroundClosure) adviceArgs[0];
Object[] closureState = aroundClosure.getState();
if (closureState.length == 3
&& closureState[0] instanceof Authenticator
&& closureState[1] instanceof String
&& closureState[2] instanceof String
) {
System.out.println("[WARN] Disabling probable authentication hack: " + thisJoinPointStaticPart);
try {
return aroundClosure.run(closureState);
} catch (Throwable t) {
throw new SoftException(t);
}
}
}
return proceed();
}
}
输出与上面保持相同,但如果在黑客方面有多个建议,甚至是多个黑客方面,您会看到差异。这个版本正在缩小范围。如果你想要这个取决于你。我建议你使用更简单的版本。在这种情况下,您只需小心更新切入点以始终拥有最新的白名单。
很抱歉回答冗长,但我发现这个问题很有趣,并试图尽可能好地解释我的解决方案。