Considérons une application JavaScript frontale dans laquelle les éléments de menu devaient être affichés ou masqués en fonction d'une logique quelque peu simple (rôles de l'utilisateur et autre état logique).
Un langage simple a été introduit pour définir cette logique de manière concise et lisible par l'homme et chaque élément de menu a été affecté à une condition de chaîne, qui ressemble à ceci: "isLoggedIn() AND NOT role(PARTNER)"
.
Afin de vérifier réellement cette condition, un simple compilateur a été implémenté, qui traduit ce langage en une chaîne de code JavaScript valide et l'exécute en utilisant eval()
en renvoyant le résultat booléen à la fin.
Le compilateur fonctionne en remplaçant certaines constructions, comme AND
, OR
et NOT
par des équivalents JavaScript valides comme &&
, ||
Et !
En utilisant des expressions régulières simples:
private compileExpression(expression: string) {
// Adding commas around function arguments (to make them strings)
expression = expression.replace(/(\w+)\((.*?)\)/g, `$1('$2')`);
// Replacing "AND"
expression = expression.replace(/\sAND\s/g, ' && ');
// Replacing "OR"
expression = expression.replace(/\sOR\s/g, ' || ');
// Replacing "NOT"
expression = expression.replace(/(\s|^)NOT\s/g, ' !');
// Prefixing predicates
expression = expression.replace(/(\w+)\((.*?)\)/g, 'Π.$1($2)');
return expression;
}
Ceci transforme "isLoggedIn() AND NOT role(PARTNER)"
en "Π.isLoggedIn() && !Π.role('PARTNER)"
, qui est ensuite exécuté par eval()
:
public matchCondition = (condition: string): boolean => {
// Using greek letter "P" for predicate (for shortness and uniqueness)
// noinspection NonAsciiCharacters
const Π = this.predicates;
const expression = this.compileExpression(condition);
const result = eval(expression);
return result;
}
Le Π
Est un objet avec des fonctions de prédicat simples, qui renvoient des valeurs booléennes comme isLoggedIn(): boolean
et role(roleName: string): boolean
.
Les expressions, qui sont traduites et exécutées, sont stockées statiquement dans l'objet local sous forme de chaînes et ne sont pas accessibles à partir du contexte global. De plus, toutes les expressions sont écrites par les développeurs et ne proviennent en aucun cas des utilisateurs de l'application.
Est-il sûr d'utiliser eval()
de cette façon ou doit-il être évité à tout prix (par exemple "eval is evil", "never use eval", etc)?
Quels sont les vecteurs d'attaque possibles, qui pourraient être utilisés pour compromettre une telle utilisation de l'évaluation?
Si des chaînes d'expression seront chargées à partir du serveur HTTPS à l'aide de XHR/Fetch, cela changera-t-il la situation en termes de sécurité?
La raison pour introduire un tel langage et ne pas définir directement les règles dans le code est qu'il fallait que ces conditions puissent être définies dans des valeurs de chaîne, par exemple dans un fichier JSON. L'autre raison est qu'une telle langue est plus facile à lire en un coup d'œil.
L'utilisation de eval
dans ce contexte ne crée aucune vulnérabilité, tant qu'un attaquant ne peut pas interférer avec les arguments passés à matchCondition
.
Si vous trouvez qu'il est plus facile de le lire/le programmer de cette façon, et que vous êtes sûr qu'aucune entrée non fiable n'entrera jamais dans votre compilateur d'expression, alors allez-y.
eval
n'est pas mauvais, les données non fiables le sont.
Veuillez noter qu'il est tout à fait possible d'éviter eval
, en extrayant les prédicats puis en les manipulant avec vos fonctions personnalisées, par exemple:
if (predicate === 'isLoggedIn()') {
return Π.isLoggedIn();
}
Aujourd'hui, tout est écrit par les développeurs. Le mois prochain ou l'année prochaine, quelqu'un dira "hé, pourquoi ne pas laisser les utilisateurs les écrire eux-mêmes?" Bam.
De plus, même si les règles sont écrites uniquement par les développeurs, incluent-elles ou incluront-elles des données provenant de l'utilisateur? Quelque chose comme des titres, des noms, des catégories, par exemple? Cela pourrait rapidement conduire à une attaque XSS.
Vos expressions régulières sont tellement "ouvertes" (en utilisant beaucoup de .*
sans aucune validation) que si quelque chose de fâcheux entre, il passera directement au eval
en une minute.
À tout le moins, si vous voulez conserver eval
, vous devriez avoir des expressions beaucoup plus strictes au lieu de .*
. Mais ceux-ci peuvent rapidement devenir soit difficiles à comprendre, soit un obstacle pour de nombreux cas pratiques.
Si quelque chose ressemble à une expression sûre, les gens vont probablement la traiter comme telle. Si un champ ressemble à n'importe quel autre champ de données, les gens (même les développeurs) y mettront probablement des données non fiables. Si quelque chose est évalué avec un accès complet aux applications, il devrait ressembler à du code.
Un autre problème réside dans les bogues subtils de votre précompilateur, qui pourraient introduire des bogues indésirables/des failles de sécurité. La plupart des vulnérabilités commencent par des bogues indésirables, avant qu'un attaquant malveillant ne puisse exploiter quelque chose. Et un nouveau méta-langage sans vérification/tests appropriés et syntaxe fortement définie n'est qu'une autre couche de confusion et de bugs qui attendent de se produire.
Si seuls les développeurs écrivent du code pour vos conditions, pourquoi ne pas simplement utiliser du JavaScript simple? La méta-langue n'apporte pratiquement aucun avantage. Et le code est géré par des gens comme le code.
Le javascript frontal lui-même est entièrement à la volonté du client exécutant le code. Si vous dépendez du javascript frontal pour la sécurité, vous avez déjà échoué à sécuriser votre application. Oubliez l'eval. Le client peut remplacer l'intégralité de votre site Web par sa propre implémentation s'il le souhaite. Ainsi, votre serveur devrait valider tout ce que le client lui demande de faire.
Dans votre cas, vous devez vous demander s'il s'agit d'une violation de la sécurité pour les utilisateurs de voir les éléments de menu qu'ils ne sont pas assez privilégiés pour voir. Si tel est le cas, le serveur ne doit pas fournir ces éléments de menu au client, car les utilisateurs peuvent consulter tout javascript que vous leur fournissez. Si ce n'est pas le cas, vous n'avez aucun problème de sécurité - comme d'autres l'ont mentionné, eval n'est "mauvais" que s'il est exécuté avec du code non sécurisé. Étant donné que votre code évalué est actuellement nettoyé, je ne pense pas que vous ayez des problèmes de sécurité.
Comme d'autres l'ont dit, tant que vos règles ne proviennent que de développeurs de confiance, il ne devrait pas y avoir de failles de sécurité lors de l'utilisation de eval
.
Cependant, eval
a beaucoup d'autres inconvénients, en termes de complexité, de maintenabilité, de débogage, etc. Et l'utilisation d'expressions régulières plus eval
pourrait facilement entraîner des problèmes sur la route, selon votre application se développe.
Je pense aussi qu'il est possible que vous surestimiez la difficulté d'écrire votre propre interprète; Les bibliothèques open-source sont suffisamment matures pour que vous puissiez créer un joli interpréteur Nice même si (comme moi) vous n'êtes pas bien familiarisé avec l'implémentation de l'analyseur et du compilateur. Voici une grammaire pour PEG.js que j'ai composée en 15 minutes environ en fonction de la description de votre problème. À des fins de démonstration, il renvoie simplement les noms de prédicat; pour l'utiliser, vous le changeriez pour retourner une fonction qui prend l'objet prédicats et invoque le prédicat approprié, mais j'espère que cela suffit pour vous donner une idée d'une approche possible.
OrExpression
= head:AndExpression tail:(_ ("OR") _ AndExpression)* {
return tail.reduce(function(result, element) {
return result || element[3];
}, head);
}
AndExpression
= head:NotExpression tail:(_ ("AND") _ NotExpression)* {
return tail.reduce(function(result, element) {
return result && element[3];
}, head);
}
NotExpression
= "NOT" _ expr:NotExpression { return !expr; }
/ "(" _ expr:OrExpression _ ")" { return expr; }
/ Predicate;
Predicate
= _ predicate:[A-Z]+ _ "(" _ arg:[A-Z]* _ ")" {
return predicate.join('') + '(' + arg.join('') + ')';
}
_ "whitespace"
= [ \t\n\r]*