1+ import ast
12from typing import Any
23from lf_toolkit .preview import Result , Params , Preview
34
4- def preview_function (response : Any , params : Params ) -> Result :
5- """
6- Function used to preview a student response.
7- ---
8- The handler function passes three arguments to preview_function():
5+ _BLOCKED_MODULES = {
6+ "os" , "sys" , "subprocess" , "socket" , "urllib" , "http" ,
7+ "requests" , "shutil" , "pathlib" , "ftplib" , "smtplib" ,
8+ "ctypes" , "multiprocessing" , "threading" , "importlib" ,
9+ "pickle" , "builtins" ,
10+ }
11+
12+ _BLOCKED_BUILTINS = {"exec" , "eval" , "compile" , "open" , "__import__" , "input" }
13+
14+
15+ class _SecurityVisitor (ast .NodeVisitor ):
16+ def __init__ (self ):
17+ self .violations : list [str ] = []
918
10- - `response` which are the answers provided by the student.
11- - `params` which are any extra parameters that may be useful,
12- e.g., error tolerances.
19+ def visit_Import (self , node ):
20+ for alias in node .names :
21+ root = alias .name .split ("." )[0 ]
22+ if root in _BLOCKED_MODULES :
23+ self .violations .append (f"import of '{ root } ' is not allowed" )
24+ self .generic_visit (node )
1325
14- The output of this function is what is returned as the API response
15- and therefore must be JSON-encodable. It must also conform to the
16- response schema.
26+ def visit_ImportFrom (self , node ):
27+ if node .module :
28+ root = node .module .split ("." )[0 ]
29+ if root in _BLOCKED_MODULES :
30+ self .violations .append (f"import of '{ root } ' is not allowed" )
31+ self .generic_visit (node )
1732
18- Any standard python library may be used, as well as any package
19- available on pip (provided it is added to requirements.txt).
33+ def visit_Call (self , node ):
34+ if isinstance (node .func , ast .Name ) and node .func .id in _BLOCKED_BUILTINS :
35+ self .violations .append (f"use of '{ node .func .id } ()' is not allowed" )
36+ self .generic_visit (node )
2037
21- The way you wish to structure you code (all in this function, or
22- split into many) is entirely up to you.
23- """
38+ def visit_Attribute (self , node ):
39+ if node .attr .startswith ("__" ) and node .attr .endswith ("__" ):
40+ self .violations .append (f"access to '{ node .attr } ' is not allowed" )
41+ self .generic_visit (node )
2442
43+
44+ def preview_function (response : Any , params : Params ) -> Result :
2545 try :
26- return Result (preview = Preview (sympy = response ))
27- except Exception as e :
28- return Result (preview = Preview (feedback = str (e )))
46+ tree = ast .parse (str (response ))
47+ except SyntaxError as e :
48+ return Result (preview = Preview (feedback = f"SyntaxError: { e .msg } (line { e .lineno } )" ))
49+
50+ visitor = _SecurityVisitor ()
51+ visitor .visit (tree )
52+ if visitor .violations :
53+ lines = "\n " .join (f"- { v } " for v in visitor .violations )
54+ return Result (preview = Preview (feedback = f"Unsafe code detected:\n { lines } " ))
55+
56+ return Result (preview = Preview (feedback = "Valid Python syntax." ))
0 commit comments