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
        }
    }
    );
}
}