Application Development Blog Posts
Learn and share on deeper, cross technology development topics such as integration and connectivity, automation, cloud extensibility, developing at scale, and security.
cancel
Showing results for 
Search instead for 
Did you mean: 
Former Member

as pointed out by horst.keller in one of his blogs, security is important and nothing which should be an afterhtought. Also it is nothing that can be solved by SAP alone but a topic for each and everyone developing code. However SAP has quite some functionality, that assist you to counter attacks more easily.

In this blog, I will show you, how to protect your code against directory traversal attacks when dealing with files.

Directory traversal attacks often also called path traversal attacks try to abuse insufficient sanitization and validation when taking user input as (part of) filenames. One simple example could be the ability to create a file with some input on the application server. A very simple program to do this could look like shown below:

REPORT Z_UPLOAD_NO_CHECK.
PARAMETERS: pv_fname TYPE c LENGTH 64 LOWER CASE DEFAULT 'test.txt',
            pv_text 
type string LOWER CASE DEFAULT 'This is the file contents'.
DATA        lv_full_name TYPE string.

AT SELECTION-SCREEN
.
 
if pv_fname = ''.
   
MESSAGE 'You need to specify a file name.' TYPE 'E'.
 
ENDIF.
  lv_full_name
= '/tmp/' && pv_fname. "Assume enduser inputs only a relative file name.

START-OF-SELECTION
.
 
OPEN DATASET lv_full_name FOR OUTPUT IN TEXT MODE ENCODING UTF-8 .
 
IF SY-SUBRC <> 0.
     
MESSAGE 'Could not open file'&& lv_full_name TYPE 'E'.
 
ELSE.
     
TRANSFER pv_text TO lv_full_name.
     
CLOSE DATASET lv_full_name.
     
Write :/ 'contents was saved to:', lv_full_name.
 
ENDIF.

What the program does, is simply asking for a file name and the content for the file. It just assumes, that the file name will be relative (like test.txt) and not absolute (like C:\TEMP\test.txt or /tmp/test.txt) and therefore just concatenate the directory the files shall be placed into and the filename itself. In the end it will open the file for writing and put the input of the user into the file.

What’s wrong with this? Two things:

  • The developer defines the directory where to place the files. If the directory does not exist, the admin can only request a new program.
  • The code does not check, whether the file will really end up in /tmp or somewhere else on the server.

Let’s have a closer look at the second issue. Let’s assume we use a filename like ../etc/passwd. What happens? The code will concatenate ‘/tmp/’ and ‘../etc/passwd’ into ‘/tmp/../etc/passwd’. In case the operating system is unix based, the system would understand ‘/etc/passwd’ as the file intended to be accessed. It is obvious, that this was not the intention of the programmer. In fact, the attacker would be able to access many operating system files which definitly should be considered a risk using this program.

So how can we avoid this. For sure, you can just search for ‘../ ‘ occurring in the input and remove that part. However if you are on a Windows based OS, this is not ‘../’ but ‘..\’ and also what happens if there are more dots, . In addition, the characters may be encoded for instance using UTF-8 and therefore look even different again. So doing this by searching just for some characters, is not that easy. You can have a look at the Wikipedia article on directory traversal attack to get an idea what is involved to protect your code.

To acounter such risks, the ABAP application server has capabilities to do this for you. As part of the function group SFIL, there is the transaction file, where you can define logical directories and logical filenames and there are function modules like file_get_name and file_validate_name to use the information captured with file to create filenames or verify, whether they are valid. This functionality is Unicode enabled, OS aware and is able to understand the input and interpret it, like an operating system would do.

Especially when using file_get_name, you may want to check your SP level, as there have been significant improvments done there. Please check SAP note 1497003 for more details.

The SFil Infrastructure

The function group SFil provides the functionality to easily handle access to files, by providing  logical filenames to developers and functions to map these logical filenames to physical filenames by the administrator.

Logical Filenames

Logical filenames in ABAP are patterns used to create or verify filenames used on a server or client system. Logical filenames include information about

·         The name to refer to the logical filename

·         The pattern for the filename

·         A data format (ASCII, Binary, Directory, …)

·         Application area

·         Optional: a logical path

The filename portion defined can be absolute (start from a root) or relative. If you want to specify filenames outside the working directory of the app server, I suggest you use the logical directories.

Filename pattern may include predefined patterns like date, time, OS or patterns based on profile parameters and more. You can even have the program provide his own input to the parameters using the parameters <PARAM_1> to <PARAM_3>. However I would recommend not to make use of these parameters for file name validation.

Logical Paths

Logical paths are an abstraction to the directories on different operating systems. The advantage here is the ability to specify one directory per OS, even though you only have one common logical directory name. For instance SAP delivers a logical path called ‘TMP’, which already contains the directories to be used to place temporary files on unix and windows operating systems.

File_Get_Name

Using the function module file_get_name, you can create filenames based on definitions in transaction file. You have to provide the logical filename and will get back the filename based on the definition in file. Please note, that the filename will only contain the directory if you request this from file_get_name explicitly.

File_Validate_name

File_validate_name checks whether a given filename is within a directory specified. You have to provide the filename to be checked and a logical filename to check it against. If you want to check, whether a given file is within a defined directory, you have to define a logical filename of type ‘DIR’ (directory). File_validate_name will not accept logical directories directly.

If the filename given is not absolute (for instance ‘test.txt’), file_validate_name will make it an absolute one by adding the working directory of the application server (profile parameter DIR_HOME). For this reason, you need to put the intended directory in front of relative filenames before calling file_validate_name.

File_validate_name will also accept all filenames which are at a deeper level of the hierarchy. This implies that the file/tmp/foo /bar would be a valid file when compared to /tmp in the sense of file_validate_name.

Doing it right

Getting back to the example above, we have to permit the admin to define where the files are placed and validate the filenames or let the system generate them.

Let’s first have a look at how generating filenames works. There are two options, how the program can be changed:

  • using a predefined filename for the file to be created
  • verifying the filename provided by the user

Using a predefined filename defined by the admin

In this case you can create a logical filename like the one below in transaction file. This definition will result in filenames that are valid on unix and windows systems and will contain the date and time of the creation of the filename.

If we would call file_get_name on 2013-08-06 at 17:35:15, the following filename would be created:

Operating SystemFile Name
Unix

/tmp/test-20130801-173515.txt

WindowsC:\Windows\Temp\ test-20130801-173515.txt

Please note, the path on windows is dependent on the profile parameter for temporary files. So the path may look different on your system.

Using file_get_name and the definition as shown above, we can now create a safe version of the program.

REPORT Z_UPLOAD_FILE_GET_NAME.
PARAMETERS pv_text  type string LOWER CASE DEFAULT 'This is the file contents'.
DATA       lv_full_name TYPE string.

AT SELECTION-SCREEN.

START-OF-SELECTION
.* get the filename including the path from the configuration
 
CALL FUNCTION 'FILE_GET_NAME'
   
EXPORTING
      LOGICAL_FILENAME             
= 'ZMY_OUTFILE'
      INCLUDING_DIR                
= 'X'
  
IMPORTING
      FILE_NAME                    
= lv_full_name
  
EXCEPTIONS
     FILE_NOT_FOUND               
= 1
    
OTHERS                        = 4.
 
IF SY-SUBRC <> 0.
     
MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno
       
WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4.
 
ENDIF.

 
OPEN DATASET lv_full_name FOR OUTPUT IN TEXT MODE ENCODING UTF-8 .
 
IF SY-SUBRC <> 0.
     
MESSAGE 'Could not open file' && lv_full_name TYPE 'E'.
 
ELSE.
     
TRANSFER pv_text TO lv_full_name.
     
CLOSE DATASET lv_full_name.
     
Write :/ 'contents was saved to:', lv_full_name.
 
ENDIF.

Verifying the filename provided by the user

In the previous example, the admin has complete control over the files created, however from an application point of view, this may not be flexible enough.

When the user shall be able to specify a file name, you need to make use of file_validate_name. In this case the admin defines a permitted directory using transaction file.

Please note, that although you need to specify a directory here, you will have to create this under logical filenames. Also you must specify ‘DIR’ as being the data type for this entry.

The example again will make use of the predefined logical directory TMP.

There are some traps you need to get around when using file_validate_name.

File_validate_name works with absolute filenames only. Therefor you have to make relatives filenames absolute first. Also you must specify the INCLUDING_DIR parameter on file_get_name when accessing filenames of type 'DIR', as otherwise the function will dump.

A safe version for the program could then look like this:

REPORT Z_UPLOAD_FILE_VALIDATE_NAME.

PARAMETERS: pv_fname       TYPE c LENGTH 64 LOWER CASE DEFAULT 'test.txt',
            pv_text       
TYPE string LOWER CASE DEFAULT 'This is the file content.'.
CONSTANTS   lc_log_fname   LIKE FILENAME-FILEINTERN VALUE 'ZMY_OUTDIR'.
DATA:       lv_full_name   TYPE string,             " filename
           lv_defaultpath TYPE string.
INITIALIZATION.* get the path, where the files should be placed from the configuration
 
CALL FUNCTION 'FILE_GET_NAME'
   
EXPORTING
      LOGICAL_FILENAME           
= lc_log_fname
      INCLUDING_DIR              
= 'X'
   
IMPORTING
      FILE_NAME                  
= lv_defaultpath
   
EXCEPTIONS
     
OTHERS = 1.
 
IF sy-subrc <> 0.
   
MESSAGE ID sy-msgid TYPE 'I' NUMBER sy-msgno
   
WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4.
 
ENDIF.

AT SELECTION-SCREEN.
 
IF pv_fname = ''.
   
MESSAGE 'You need to specify a file name.' TYPE 'E'.
 
ENDIF.
* check whether the file name is a relative one
 
IF cl_fs_path=>create( pv_fname )->is_relative( ) = abap_true.
   
" file name is a relative one, put the default path before the filename
   
" we also need to check in this case, as '/tmp' + '../etc/test.txt' is still outside /tmp
    lv_full_name
= lv_defaultpath && pv_fname.
 
ELSE.
   
" file name is already absolute, just use it.
    lv_full_name
= pv_fname.
 
ENDIF.

 
CALL FUNCTION 'FILE_VALIDATE_NAME'
   
EXPORTING
      LOGICAL_FILENAME           
= lc_log_fname
   
CHANGING
      PHYSICAL_FILENAME          
= lv_full_name
   
EXCEPTIONS
      LOGICAL_FILENAME_NOT_FOUND 
= 2
      VALIDATION_FAILED          
= 1
     
OTHERS                      = 4.
 
IF SY-SUBRC <> 0.
   
IF SY-SUBRC > 1.
     
MESSAGE ID sy-msgid TYPE 'A' NUMBER sy-msgno
       
WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4.
   
ELSE.
     
MESSAGE 'The filename >' && pv_fname && '< is not valid, please try again.' TYPE 'E'.
   
ENDIF.
 
ENDIF.

START-OF-SELECTION.
 
OPEN DATASET lv_full_name FOR OUTPUT IN TEXT MODE ENCODING UTF-8 .
 
IF SY-SUBRC <> 0.
     
MESSAGE 'Could not open file' && lv_full_name TYPE 'E'.
 
ELSE.
     
TRANSFER pv_text TO lv_full_name.
     
CLOSE DATASET lv_full_name.
     
Write :/ 'contents was saved to:', lv_full_name.
 
ENDIF.

If you need more information on the functions used above to protect against directory traversal attacks, you can allways have a look at the secure programming guide, the documentation for transaction FILE, for the function module file_get_name and for the function module file_validate_name.

3 Comments