Esigenza Reale
Mettere in sicurezza ambienti CMS dove gli utenti “Power User” possono modificare parzialmente i template HTML.
Analisi Tecnica
Problema: Un utente malintenzionato potrebbe inserire un’espressione nel database che, una volta renderizzata, esegue codice sulla JVM.
Perché: Sandbox dell’engine SpEL. Ho scelto di limitare le classi risolvibili dal compilatore SpEL per impedire l’invocazione di metodi di sistema pericolosi.
Esempio Implementativo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/* Implemento un TypeLocator restrittivo che blocca l'accesso alle classi
pericolose. Il TypeLocator è il componente SpEL responsabile di risolvere
T(java.lang.Runtime): sostituendolo con uno personalizzato, posso bloccare
selettivamente le classi. */
public class RestrictedTypeLocator implements TypeLocator {
private static final Set<String> BLOCKED_PACKAGES = Set.of(
"java.lang.Runtime", "java.lang.ProcessBuilder", "java.lang.Process",
"java.lang.reflect", "java.lang.ClassLoader", "sun.misc.Unsafe",
"java.io.File", "java.nio.file", "java.net.URL", "java.net.Socket" );
@Override public Class<?> findType(String typeName) throws
EvaluationException {
// Blocco l'accesso a qualsiasi classe nei package pericolosi for
(String blocked : BLOCKED_PACKAGES)
{
if (typeName.startsWith(blocked)) {
throw new EvaluationException( "Accesso negato alla classe: " +
typeName + ". Tipo non consentito nel contesto sandbox.");
}
}
try {
return ClassUtils.forName(typeName,
ClassUtils.getDefaultClassLoader());
}
catch (ClassNotFoundException e) {
throw new EvaluationException("Classe non trovata: " + typeName);
}
}
}
/* Configuro il contesto di valutazione SpEL sandbox per il motore del CMS: */
@Configuration public class SpelSandboxConfig {
@Bean public SpelSandboxEvaluator spelSandboxEvaluator() {
return new SpelSandboxEvaluator();
}
}
@Service public class SpelSandboxEvaluator {
private final SpelExpressionParser parser = new SpelExpressionParser();
public Object evaluate(String expression, Map<String, Object> variables) {
StandardEvaluationContext context = new StandardEvaluationContext();
// Sostituisco il TypeLocator di default con quello restrittivo
context.setTypeLocator(new RestrictedTypeLocator())
;
// Imposto solo le variabili esplicitamente consentite: zero bean Spring
accessibili variables.forEach(context::setVariable)
;
// NON chiamo context.setBeanResolver(): impedisco l'accesso ai bean
Spring try
{
Expression expr = parser.parseExpression(expression);
return expr.getValue(context);
}
catch (EvaluationException e) {
log.warn("Espressione SpEL bloccata: '{
}
' - Motivo: {
}
", expression, e.getMessage());
throw new SecurityException("Espressione non consentita: " +
e.getMessage());
}
}
}
/* Uso il valutatore sandbox nel servizio del CMS: */
@Service public class CmsTemplateService {
@Autowired private SpelSandboxEvaluator sandbox;
public String renderCmsBlock(CmsBlock block, PageContext pageContext) {
// Valuto le espressioni SpEL presenti nel template CMS in modo sicuro
Map<String, Object> safeVariables = Map.of( "page",
pageContext.getPageData(), // Solo i dati della pagina "user",
pageContext.getPublicUserData() // Solo i dati pubblici dell'utente
// NON espongo: applicationContext, environment, system properties )
;
// Processo le espressioni $
{
...
}
nel contenuto del blocco CMS return
processExpressions(block.getContent(), safeVariables);
}
private String processExpressions(String content, Map<String, Object> vars)
{
// Pattern per trovare le espressioni $
{
...
}
nel contenuto del CMS return content.replaceAll("\\$\\{
([^
}
]+)
}
", match -> {
String expression = match.replaceAll("\\$\\{
|
}
", "");
try {
Object result = sandbox.evaluate(expression, vars);
return result != null ? result.toString() : "";
}
catch (SecurityException e) {
return "[Espressione non consentita]";
// Non espongo l'errore all'utente finale
}
}
);
}
}