Browse Source

avoid os and sys dependency, check module relative path

Johann ELSASS 1 year ago
parent
commit
0ff4d552d2
3 changed files with 236 additions and 119 deletions
  1. 235 110
      lazpaint/upython.pas
  2. 0 7
      resources/scripts/lazpaint/command.py
  3. 1 2
      resources/scripts/lazpaint/image.py

+ 235 - 110
lazpaint/upython.pas

@@ -35,6 +35,7 @@ type
     procedure PythonError(ALine: RawByteString);
     procedure PythonError(ALine: RawByteString);
     procedure PythonOutput(ALine: RawByteString);
     procedure PythonOutput(ALine: RawByteString);
     procedure PythonBusy(var {%H-}ASleep: boolean);
     procedure PythonBusy(var {%H-}ASleep: boolean);
+    function CheckScriptAndDependencySafe(AFilename: UTF8String; APythonVersion: integer): boolean;
   public
   public
     constructor Create(APythonBin: string = DefaultPythonBin);
     constructor Create(APythonBin: string = DefaultPythonBin);
     procedure Run(AScriptFilename: UTF8String; APythonVersion: integer = 3);
     procedure Run(AScriptFilename: UTF8String; APythonVersion: integer = 3);
@@ -51,7 +52,7 @@ type
 
 
 function GetPythonVersion(APythonBin: string = DefaultPythonBin): string;
 function GetPythonVersion(APythonBin: string = DefaultPythonBin): string;
 function GetScriptTitle(AFilename: string): string;
 function GetScriptTitle(AFilename: string): string;
-function CheckPythonScriptSafe(AFilename: string; out AUnsafeModules: TStringList): boolean;
+function CheckPythonScriptSafe(AFilename: string; out ASafeModules, AUnsafeModules: TStringList): boolean;
 
 
 var
 var
   CustomScriptDirectory: string;
   CustomScriptDirectory: string;
@@ -167,7 +168,7 @@ begin
   end;
   end;
 end;
 end;
 
 
-function CheckPythonScriptSafe(AFilename: string; out AUnsafeModules: TStringList): boolean;
+function CheckPythonScriptSafe(AFilename: string; out ASafeModules, AUnsafeModules: TStringList): boolean;
   function binarySearch(x: string; a: array of string): integer;
   function binarySearch(x: string; a: array of string): integer;
   var  L, R, M: integer;  // left, right, middle
   var  L, R, M: integer;  // left, right, middle
   begin
   begin
@@ -184,7 +185,7 @@ function CheckPythonScriptSafe(AFilename: string; out AUnsafeModules: TStringLis
     Exit(-1) // did not found x in a
     Exit(-1) // did not found x in a
   end;
   end;
 
 
-  function idOk(AId: string; var isImport: integer): boolean;
+  function idOk(AId: string; var importCount: integer): boolean;
   const forbidden: array[0..6] of string =
   const forbidden: array[0..6] of string =
   ('__import__',
   ('__import__',
    'compile',
    'compile',
@@ -194,7 +195,7 @@ function CheckPythonScriptSafe(AFilename: string; out AUnsafeModules: TStringLis
    'globals',
    'globals',
    'locals');
    'locals');
   begin
   begin
-    if AId = 'import' then inc(isImport);
+    if AId = 'import' then inc(importCount);
     exit(binarySearch(AId, forbidden) = -1);
     exit(binarySearch(AId, forbidden) = -1);
   end;
   end;
 
 
@@ -202,45 +203,61 @@ function CheckPythonScriptSafe(AFilename: string; out AUnsafeModules: TStringLis
   const ContinueIdentifier = ['A'..'Z','a'..'z','_','0'..'9'];
   const ContinueIdentifier = ['A'..'Z','a'..'z','_','0'..'9'];
   const WhiteSpace = [' ', #9];
   const WhiteSpace = [' ', #9];
 
 
-  function importOk(const s: string; isImport: integer; previousBackslash: boolean): boolean;
-  const forbiddenModules: array[0..23] of string =
-  ('ast',
-   'builtins',
-   'code',
-   'codecs',
-   'ctypes',
-   'ftplib',
-   'gc',
-   'io',
-   'multiprocessing',
-   'os',
-   'pathlib',
-   'poplib',
-   'pty',
-   'runpy',
-   'shutil',
-   'smtplib',
-   'socket',
-   'subprocess',
-   'sys',
-   'telnetlib',
-   'tempfile',
-   'threading',
-   'wsgiref',
-   'xmlrpc');
-
-  const safeModules: array[0..10] of string =
-  ('PIL',
-   'calendar',
-   'datetime',
-   'decimal',
-   'fractions',
+  function importOk(const s: string; importCount: integer; previousBackslash: boolean): boolean;
+  const ForbiddenModules: array[0..22] of string =
+  ('builtins',          // Provides direct access to all built-in identifiers of Python.
+   'code',              // Facilities to implement interactive Python interpreters.
+   'codecs',            // Core support for encoding and decoding text and binary data.
+   'ctypes',            // Create and manipulate C-compatible data types in Python, and call functions in dynamic link libraries/shared libraries.
+   'ftplib',            // Interface to the FTP protocol.
+   'gc',                // Interface to the garbage collection facility for reference cycles.
+   'io',                // Core tools for working with streams (core I/O operations).
+   'multiprocessing',   // Process-based parallelism.
+   'os',                // Interface to the operating system, including file and process operations.
+   'pathlib',           // Object-oriented filesystem paths.
+   'poplib',            // Client-side support for the POP3 protocol.
+   'pty',               // Operations for handling the pseudo-terminal concept.
+   'runpy',             // Locating and running Python programs using various modes of the `__main__` module.
+   'shutil',            // High-level file operations, including copying and deletion.
+   'smtplib',           // Client-side objects for the SMTP and ESMTP protocols.
+   'socket',            // Low-level networking operations.
+   'subprocess',        // Spawn additional processes, connect to their input/output/error pipes, and obtain their return codes.
+   'sys',               // Access and set variables used or maintained by the Python interpreter.
+   'telnetlib',         // Client-side support for the Telnet protocol.
+   'tempfile',          // Generate temporary files and directories.
+   'threading',         // Higher-level threading interfaces on top of the lower-level `_thread` module.
+   'wsgiref',           // WSGI utility functions and reference implementation.
+   'xmlrpc'             // XML-RPC server and client modules.
+  );
+
+  const SafeModules: array[0..26] of string =
+  ('PIL',           // Python Imaging Library, for image processing.
+   'array',         // Basic mutable array operations.
+   'ast',           // Abstract Syntax Trees
+   'bisect',        // Algorithms for manipulating sorted lists.
+   'calendar',      // Functions for working with calendars and dates.
+   'collections',   // Container datatypes like namedtuples and defaultdict.
+   'colorsys',      // Color system conversions.
+   'copy',          // Shallow and deep copy operations.
+   'csv',           // Reading and writing CSV files.
+   'datetime',      // Basic date and time types.
+   'decimal',       // Fixed and floating point arithmetic using decimal notation.
+   'enum',          // Enumerations in Python.
+   'fractions',     // Rational numbers.
+   'functools',     // Higher-order functions and operations on callable objects.
+   'hashlib',       // Secure hash and message digest algorithms.
+   'itertools',     // Functions for creating iterators for efficient looping.
+   'json',          // Encoding and decoding JSON format.
    'lazpaint',
    'lazpaint',
-   'math',
-   'platform',
-   'statistics',
-   'time',
-   'tkinter');
+   'math',          // Mathematical functions.
+   'platform',      // Access to platform-specific attributes and functions.
+   'queue',         // A multi-producer, multi-consumer queue.
+   'random',        // Generate pseudo-random numbers.
+   'statistics',    // Mathematical statistics functions.
+   'string',        // Common string operations.
+   'time',          // Time-related functions.
+   'tkinter',       // Standard GUI library for Python.
+   'uuid');         // UUID objects
 
 
   procedure SkipSpaces(var idx: integer);
   procedure SkipSpaces(var idx: integer);
   begin
   begin
@@ -257,39 +274,33 @@ function CheckPythonScriptSafe(AFilename: string; out AUnsafeModules: TStringLis
     idx := idxEnd;
     idx := idxEnd;
   end;
   end;
 
 
-  var idx: integer;
-    importAfter: boolean;
-    moduleName, subId: string;
+  function SkipAs(var idx: integer): boolean;
+  var
+    subId: String;
   begin
   begin
-    if isImport <> 1 then exit(false); // syntax error
-
-    if s.StartsWith('from ') then
-    begin
-      idx := length('from ') + 1;
-      importAfter := true;
-    end else
-    if s.StartsWith('import ') then
-    begin
-      if previousBackslash then exit(false); // could be an exploit
-      idx := length('import ') + 1;
-      importAfter := false;
-    end
-    else
-      exit(false); // syntax error
-
     SkipSpaces(idx);
     SkipSpaces(idx);
-    moduleName := GetId(idx);
-    if moduleName = '' then exit(false); // syntax error
-    // check if module is allowed
-    if binarySearch(moduleName, forbiddenModules) <> -1 then exit(false);
-    if binarySearch(moduleName, safeModules) = -1 then
+    if (idx > length(s)) or (s[idx] = '#') then exit(true);
+    subId := GetId(idx);
+    if subId = 'as' then
     begin
     begin
-      if AUnsafeModules = nil then
-         AUnsafeModules := TStringList.Create;
-      if AUnsafeModules.IndexOf(moduleName) = -1 then
-        AUnsafeModules.Add(moduleName);
+      SkipSpaces(idx);
+      subId := GetId(idx);
+      if subId = '' then exit(false); // syntax error
     end;
     end;
+    exit(true);
+  end;
 
 
+  function ParseModuleName(var idx: integer; out AModuleName: string; out AIsSafe: boolean): boolean;
+  var
+    subId: String;
+  begin
+    SkipSpaces(idx);
+    AIsSafe := false;
+    AModuleName := GetId(idx);
+    if AModuleName = '' then exit(false); // syntax error
+    // check if module is allowed
+    if binarySearch(AModuleName, ForbiddenModules) <> -1 then exit(false);
+    AIsSafe := binarySearch(AModuleName, SafeModules) <> -1;
     SkipSpaces(idx);
     SkipSpaces(idx);
     // submodule
     // submodule
     while (idx <= length(s)) and (s[idx] = '.') do
     while (idx <= length(s)) and (s[idx] = '.') do
@@ -298,28 +309,82 @@ function CheckPythonScriptSafe(AFilename: string; out AUnsafeModules: TStringLis
       SkipSpaces(idx);
       SkipSpaces(idx);
       subId := GetId(idx);
       subId := GetId(idx);
       if subId = '' then exit(false); // syntax error
       if subId = '' then exit(false); // syntax error
+      AModuleName += '.' + subId;
       SkipSpaces(idx);
       SkipSpaces(idx);
     end;
     end;
+    exit(true);
+  end;
+
+  procedure AddModule(AModuleName: string; AIsSafe: boolean);
+  begin
+    if not AIsSafe then
+    begin
+      if AUnsafeModules = nil then
+         AUnsafeModules := TStringList.Create;
+      if AUnsafeModules.IndexOf(AModuleName) = -1 then
+        AUnsafeModules.Add(AModuleName);
+    end else
+    begin
+      if ASafeModules = nil then
+         ASafeModules := TStringList.Create;
+      if ASafeModules.IndexOf(AModuleName) = -1 then
+        ASafeModules.Add(AModuleName);
+    end;
+  end;
 
 
-    if importAfter then
+  var idx: integer;
+    fromClause: boolean;
+    moduleName, subId: string;
+    isSafe: boolean;
+  begin
+    if importCount <> 1 then exit(false); // syntax error
+
+    if s.StartsWith('from ') then
     begin
     begin
-      subId := GetId(idx);
-      if subId <> 'import' then exit(false); // syntax error
+      idx := length('from ') + 1;
+      fromClause := true;
     end else
     end else
+    if s.StartsWith('import ') then
     begin
     begin
-      if (idx > length(s)) or (s[idx] = '#') then exit(true);
+      if previousBackslash then exit(false); // could be an exploit
+      idx := length('import ') + 1;
+      fromClause := false;
+    end
+    else
+      exit(false); // syntax error
 
 
+    if not ParseModuleName(idx, moduleName, isSafe) then exit(false);
+
+    if fromClause then
+    begin
       subId := GetId(idx);
       subId := GetId(idx);
-      if subId = 'as' then
-      begin
+      if subId <> 'import' then exit(false); // syntax error
+      repeat
         SkipSpaces(idx);
         SkipSpaces(idx);
         subId := GetId(idx);
         subId := GetId(idx);
         if subId = '' then exit(false); // syntax error
         if subId = '' then exit(false); // syntax error
-
-        if (idx <= length(s)) and (s[idx] <> '#') then // expect end of line
-          exit(false); // syntax error
-      end;
+        AddModule(moduleName+'.'+subId, isSafe);
+        if not SkipAs(idx) then exit(false);
+        SkipSpaces(idx);
+        if (idx <= length(s)) and (s[idx] = ',') then inc(idx)
+        else break;
+      until false;
+    end else
+    begin
+      repeat
+        AddModule(moduleName, isSafe);
+        if not SkipAs(idx) then exit(false);
+        SkipSpaces(idx);
+        if (idx <= length(s)) and (s[idx] = ',') then
+        begin
+          inc(idx);
+          if not ParseModuleName(idx, moduleName, isSafe) then exit(false);
+        end
+        else break;
+      until false;
     end;
     end;
+    if (idx <= length(s)) and (s[idx] <> '#') then // expect end of line
+      exit(false); // syntax error
 
 
     exit(true);
     exit(true);
   end;
   end;
@@ -327,10 +392,10 @@ function CheckPythonScriptSafe(AFilename: string; out AUnsafeModules: TStringLis
   function lineOk(const s: string; previousBackslash: boolean): boolean;
   function lineOk(const s: string; previousBackslash: boolean): boolean;
   var
   var
     startId, i: integer;
     startId, i: integer;
-    isImport: integer;
+    importCount: integer;
   begin
   begin
     startId := -1;
     startId := -1;
-    isImport := 0;
+    importCount := 0;
 
 
     for i := 1 to length(s) do
     for i := 1 to length(s) do
     begin
     begin
@@ -341,14 +406,14 @@ function CheckPythonScriptSafe(AFilename: string; out AUnsafeModules: TStringLis
       end else
       end else
       if (startId <> -1) and not (s[i] in ContinueIdentifier) then
       if (startId <> -1) and not (s[i] in ContinueIdentifier) then
       begin
       begin
-        if not idOk(copy(s, startId, i-startId), isImport) then exit(false);
+        if not idOk(copy(s, startId, i-startId), importCount) then exit(false);
         startId := -1;
         startId := -1;
       end;
       end;
     end;
     end;
-    if (startId <> -1) and not idOk(copy(s, startId, length(s)-startId+1), isImport) then
+    if (startId <> -1) and not idOk(copy(s, startId, length(s)-startId+1), importCount) then
       exit(false);
       exit(false);
 
 
-    if (isImport > 0) and not importOk(s, isImport, previousBackslash) then exit(false);
+    if (importCount > 0) and not importOk(s, importCount, previousBackslash) then exit(false);
 
 
     exit(true);
     exit(true);
   end;
   end;
@@ -358,6 +423,7 @@ var
   s: string;
   s: string;
   previousBackslash: boolean;
   previousBackslash: boolean;
 begin
 begin
+  ASafeModules := nil;
   AUnsafeModules := nil;
   AUnsafeModules := nil;
   assignFile(t, AFilename);
   assignFile(t, AFilename);
   reset(t);
   reset(t);
@@ -467,6 +533,88 @@ begin
   if Assigned(FOnBusy) then FOnBusy(self);
   if Assigned(FOnBusy) then FOnBusy(self);
 end;
 end;
 
 
+function TPythonScript.CheckScriptAndDependencySafe(AFilename: UTF8String; APythonVersion: integer): boolean;
+var
+  filesToCheck: TStringList;
+
+  procedure AddModuleToCheck(AModuleName: UTF8String; ABasePath: UTF8String);
+  var fullPath, moduleFilename: string;
+  begin
+    fullPath := ConcatPaths([ABasePath, StringReplace(AModuleName, '.', PathDelim, [rfReplaceAll])]);
+    moduleFilename := fullPath+'.py';
+    if (filesToCheck.IndexOf(moduleFilename) = -1) and FileExists(moduleFilename) then
+      filesToCheck.Add(moduleFilename) else
+    begin
+      moduleFilename := fullPath+'\__init__.py';
+      if (filesToCheck.IndexOf(moduleFilename) = -1) and FileExists(moduleFilename) then
+        filesToCheck.Add(moduleFilename);
+      end;
+  end;
+
+var
+  safeModules, unsafeModules, allUnsafeModules: TStringList;
+  proceed: boolean;
+  curFile, i: integer;
+  curPath: string;
+
+begin
+  allUnsafeModules := TStringList.Create;
+  allUnsafeModules.Sorted := true;
+  allUnsafeModules.Duplicates:= dupIgnore;
+  filesToCheck := TStringList.Create;
+  filesToCheck.Add(AFilename);
+  curFile := 0;
+  curPath := ExtractFilePath(AFilename);
+  while curFile < filesToCheck.Count do
+  begin
+    if not CheckPythonScriptSafe(filesToCheck[curFile], safeModules, unsafeModules) then
+    begin
+      safeModules.Free;
+      unsafeModules.Free;
+      raise exception.Create('The script file does not seem to be safe: ' +
+                             filesToCheck[curFile]);
+    end;
+    if Assigned(unsafeModules) then
+    begin
+      for i := 0 to unsafeModules.Count-1 do
+      begin
+        AddModuleToCheck(unsafeModules[i], curPath);
+        allUnsafeModules.Add(unsafeModules[i]);
+      end;
+    end;
+    if Assigned(safeModules) then
+    begin
+      for i := 0 to safeModules.Count-1 do
+        AddModuleToCheck(safeModules[i], curPath);
+    end;
+    safeModules.Free;
+    unsafeModules.Free;
+    inc(curFile);
+  end;
+  filesToCheck.Free;
+
+  if allUnsafeModules.Count > 0 then
+  begin
+    proceed := true;
+    if Assigned(OnWarning) then
+    begin
+      OnWarning(self, 'Are you sure you would like to run this script? ' +
+        'The following modules used by this script may be unsafe: '+
+        allUnsafeModules.CommaText, proceed);
+    end;
+    allUnsafeModules.Free;
+    if not proceed then exit(false);
+  end else
+    allUnsafeModules.Free;
+
+  if PythonVersionMajor <> APythonVersion then
+    raise exception.Create(
+      StringReplace( StringReplace(rsPythonUnexpectedVersion,
+        '%1',inttostr(APythonVersion),[]),
+        '%2',inttostr(PythonVersionMajor),[]) + #9 + rsDownload + #9 + 'https://www.python.org');
+  exit(true);
+end;
+
 constructor TPythonScript.Create(APythonBin: string);
 constructor TPythonScript.Create(APythonBin: string);
 begin
 begin
   FPythonBin := APythonBin;
   FPythonBin := APythonBin;
@@ -495,35 +643,12 @@ end;
 
 
 procedure TPythonScript.Run(AScriptFilename: UTF8String;
 procedure TPythonScript.Run(AScriptFilename: UTF8String;
   APythonVersion: integer);
   APythonVersion: integer);
-var
-  unsafeModules: TStringList;
-  proceed: boolean;
 begin
 begin
-  if not CheckPythonScriptSafe(AScriptFilename, unsafeModules) then
-  begin
-    unsafeModules.Free;
-    raise exception.Create('The script file does not seem to be safe');
-  end;
-  if Assigned(unsafeModules) then
-  begin
-    proceed := true;
-    if Assigned(OnWarning) then
-    begin
-      OnWarning(self, 'Are you sure you would like to run this script? ' +
-        'The following modules used by this script may be unsafe: '+
-        unsafeModules.CommaText, proceed);
-    end;
-    unsafeModules.Free;
-    if not proceed then exit;
-  end;
+  if not CheckScriptAndDependencySafe(AScriptFilename, APythonVersion) then exit;
   FLinePrefix := '';
   FLinePrefix := '';
-  if PythonVersionMajor <> APythonVersion then
-    raise exception.Create(
-      StringReplace( StringReplace(rsPythonUnexpectedVersion,
-        '%1',inttostr(APythonVersion),[]),
-        '%2',inttostr(PythonVersionMajor),[]) + #9 + rsDownload + #9 + 'https://www.python.org');
   FFirstOutput:= true;
   FFirstOutput:= true;
   AutomationEnvironment.Values['PYTHONPATH'] := DefaultScriptDirectory;
   AutomationEnvironment.Values['PYTHONPATH'] := DefaultScriptDirectory;
+  AutomationEnvironment.Values['PYTHONIOENCODING'] := 'utf-8';
   try
   try
     RunProcessAutomation(FPythonBin, ['-u', AScriptFilename], FPythonSend, @PythonOutput, @PythonError, @PythonBusy);
     RunProcessAutomation(FPythonBin, ['-u', AScriptFilename], FPythonSend, @PythonOutput, @PythonError, @PythonBusy);
   finally
   finally

+ 0 - 7
resources/scripts/lazpaint/command.py

@@ -6,13 +6,6 @@ print("LazPaint script\t")
 if input('') != chr(27) + 'LazPaint': 
 if input('') != chr(27) + 'LazPaint': 
   print("Needs to be run from LazPaint.")
   print("Needs to be run from LazPaint.")
   exit()
   exit()
-  
-import sys
-if sys.platform == "win32":
-  import io
-  sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding = 'utf-8')
-  sys.stderr = io.TextIOWrapper(sys.stderr.detach(), encoding = 'utf-8')
-  sys.stdin = io.TextIOWrapper(sys.stdin.detach(), encoding = 'utf-8')
 
 
 def parse_str(text: str):
 def parse_str(text: str):
   if text[:1] == "#":
   if text[:1] == "#":

+ 1 - 2
resources/scripts/lazpaint/image.py

@@ -1,5 +1,4 @@
 from lazpaint import command, dialog, colors, layer
 from lazpaint import command, dialog, colors, layer
-import os
 
 
 if __name__ == "__main__":
 if __name__ == "__main__":
   dialog.show_message("Library to act on the whole image.")
   dialog.show_message("Library to act on the whole image.")
@@ -105,7 +104,7 @@ def export(file_name=None, validate=False, overwrite=False, skip_options=False)
   return command.send("FileSaveAs?", FileName=file_name, Validate=validate, Overwrite=overwrite, SkipOptions=skip_options, Export=True)
   return command.send("FileSaveAs?", FileName=file_name, Validate=validate, Overwrite=overwrite, SkipOptions=skip_options, Export=True)
 
 
 def change_file_extension(file_name: str, new_extension: str) -> str:
 def change_file_extension(file_name: str, new_extension: str) -> str:
-  base, ext = os.path.splitext(file_name)
+  base = file_name.rsplit('.', 1)[0]
   if len(new_extension) > 0 and new_extension[0:1] != ".":
   if len(new_extension) > 0 and new_extension[0:1] != ".":
     new_extension = "." + new_extension
     new_extension = "." + new_extension
   return base + new_extension
   return base + new_extension