Security fixes for the input-validation gaps in the public download and metadata services, plus missing in-code admin gates: - mpe_accesscheck: validate base_table (LIBDS) and access_level before they reach the authorisation query, and escape embedded quotes in the SQL literals (defence in depth for direct macro callers) - getrawdata: validate table (LIBDS / format-catalog form), filter (integer) and type before they are used; read all inputs with symget in a data step so macro content cannot execute at a resolution boundary - getdiffs: validate libds, table and stp_diffs_csv before the access check and the staging-file stream path - getcols, getcolvals, validatefilter: read the IWANT inputs with symget in a data step and validate (LIBDS / SAS name) before use - admin dirlist, refreshlibs, refreshcatalog, exportconfig: require membership of the DC administrators group (the admin folder prefix is not an access control) - admin dirlist: read parent with symget and reject macro characters Tests (all proven RED on the vulnerable services first, then GREEN on the fix): getrawdata.test.1, getdiffs.test, getcols.test, getcolvals.test.4, validatefilter.test.1, dirlist.test, refreshcatalog.test.1
195 lines
4.7 KiB
SAS
195 lines
4.7 KiB
SAS
/**
|
|
@file
|
|
@brief testing getdiffs service - input validation (security)
|
|
@details The libds, table and stp_diffs_csv request params must be
|
|
well-formed before they reach the access check and the staging file
|
|
path. An invalid value aborts the service, which shows up as a
|
|
canceled child job (an aborted service registers no webout).
|
|
|
|
A real load is staged first (stagedata) and a diffs csv written into
|
|
the staging directory, so every payload below resolves to that REAL
|
|
file when executed - on a vulnerable service the job completes, and
|
|
only the validating service cancels it. The assertions cannot pass
|
|
against a service that does not validate.
|
|
|
|
<h4> SAS Macros </h4>
|
|
@li mp_assert.sas
|
|
@li mx_execute.sas
|
|
@li mf_getuniquefileref.sas
|
|
|
|
**/
|
|
|
|
%let _program=&appLoc/services/auditors/getdiffs;
|
|
|
|
/**
|
|
* Stage a real load so a real staging directory exists
|
|
*/
|
|
data work.sascontroltable;
|
|
action='LOAD';
|
|
message="getdiffs test prep";
|
|
libds="&dclib..MPE_X_TEST";
|
|
output;
|
|
stop;
|
|
run;
|
|
|
|
proc sql noprint;
|
|
select max(primary_key_field) into: maxpk
|
|
from &dclib..mpe_x_test;
|
|
quit;
|
|
|
|
data work.jsdata;
|
|
set &dclib..mpe_x_test(rename=(
|
|
some_date=dt2 SOME_DATETIME=dttm2 some_time=tm2)
|
|
);
|
|
some_date=put(dt2,date9.);
|
|
SOME_DATETIME=put(dttm2,datetime19.);
|
|
some_time=put(tm2,time.);
|
|
drop dt2 dttm2 tm2;
|
|
if _n_=1 then do;
|
|
_____DELETE__THIS__RECORD_____='No';
|
|
some_char='getdiffs security test';
|
|
some_num=&maxpk+1;
|
|
end;
|
|
else stop;
|
|
run;
|
|
|
|
%mx_execute(&appLoc/services/editors/stagedata,
|
|
viyacontext=&defaultcontext,
|
|
inputdatasets=work.jsdata work.sascontroltable,
|
|
outlib=webstage,
|
|
mdebug=&sasjs_mdebug
|
|
)
|
|
|
|
%let stagetest=0;
|
|
data _null_;
|
|
set webstage.sasparams;
|
|
putlog (_all_)(=);
|
|
if status='SUCCESS' then call symputx('stagetest',1);
|
|
call symputx('loadref',dsid);
|
|
run;
|
|
|
|
%mp_assert(
|
|
iftrue=(&stagetest=1 and &syscc=0),
|
|
desc=stagedata succeeded in getdiffs prep,
|
|
outds=work.test_results
|
|
)
|
|
|
|
/**
|
|
* Write the diffs csv into the real staging directory
|
|
*/
|
|
%let diffscsv=tempDiffs_secrev.csv;
|
|
data _null_;
|
|
file "&dc_staging_area/&loadref./&diffscsv";
|
|
put 'SOME_CHAR,_____STATUS_____';
|
|
put 'getdiffs security test,UPDATED';
|
|
run;
|
|
|
|
/**
|
|
* Test 1 - the mpe_accesscheck SQL injection payload in libds must
|
|
* abort the service (validation fires before the authz query, so
|
|
* the authz bypass cannot happen). The payload is sent through the
|
|
* sasjs table channel (BrowserParams) like the frontend does - the
|
|
* raw-quote form is masked in plain URL params on this platform.
|
|
*/
|
|
%let fb1=%mf_getuniquefileref();
|
|
data _null_;
|
|
file &fb1 termstr=crlf;
|
|
length _row $400.;
|
|
put 'TABLE:$41. STP_DIFFS_CSV:$100. libds:$41.';
|
|
_row=cats(symget('loadref'),',',symget('diffscsv'),',',
|
|
'SOMELIB.SOMEDS',"'22'x"," or ","'22'x",'1',"'22'x",' ne ',"'22'x",'2');
|
|
put _row;
|
|
run;
|
|
|
|
%mx_execute(&_program,
|
|
viyacontext=&defaultcontext,
|
|
inputfiles=&fb1:BrowserParams,
|
|
outref=web1,
|
|
viyaresult=WEBOUT_TXT
|
|
)
|
|
|
|
%let abort1=0;
|
|
data _null_;
|
|
set work.results;
|
|
if state='canceled' then call symputx('abort1',1);
|
|
run;
|
|
|
|
%mp_assert(
|
|
iftrue=(&abort1=1),
|
|
desc=SQL injection payload in libds aborts the service,
|
|
outds=work.test_results
|
|
)
|
|
|
|
/**
|
|
* Test 2 - path traversal in table must abort the service (the
|
|
* payload resolves to the real staged file through a .. detour)
|
|
*/
|
|
data _null_;
|
|
length _dir $512;
|
|
_dir=scan(symget('dc_staging_area'),-1,'/');
|
|
call symputx('travtable',cats('../',_dir,'/','&loadref'));
|
|
run;
|
|
|
|
%let fb2=%mf_getuniquefileref();
|
|
data _null_;
|
|
file &fb2 termstr=crlf;
|
|
put 'TABLE:$41. STP_DIFFS_CSV:$100. libds:$41.';
|
|
put "&travtable.,&diffscsv.,&dclib..MPE_X_TEST";
|
|
run;
|
|
|
|
%mx_execute(&_program,
|
|
viyacontext=&defaultcontext,
|
|
inputfiles=&fb2:BrowserParams,
|
|
outref=web2,
|
|
viyaresult=WEBOUT_TXT
|
|
)
|
|
|
|
%let abort2=0;
|
|
data _null_;
|
|
set work.results;
|
|
if state='canceled' then call symputx('abort2',1);
|
|
run;
|
|
|
|
%mp_assert(
|
|
iftrue=(&abort2=1),
|
|
desc=Path traversal in table aborts the service,
|
|
outds=work.test_results
|
|
)
|
|
|
|
/**
|
|
* Test 3 - path traversal in stp_diffs_csv must abort the service
|
|
*/
|
|
%let fb3=%mf_getuniquefileref();
|
|
data _null_;
|
|
file &fb3 termstr=crlf;
|
|
put 'TABLE:$41. STP_DIFFS_CSV:$100. libds:$41.';
|
|
put "&loadref.,../&loadref./&diffscsv.,&dclib..MPE_X_TEST";
|
|
run;
|
|
|
|
%mx_execute(&_program,
|
|
viyacontext=&defaultcontext,
|
|
inputfiles=&fb3:BrowserParams,
|
|
outref=web3,
|
|
viyaresult=WEBOUT_TXT
|
|
)
|
|
|
|
%let abort3=0;
|
|
data _null_;
|
|
set work.results;
|
|
if state='canceled' then call symputx('abort3',1);
|
|
run;
|
|
|
|
%mp_assert(
|
|
iftrue=(&abort3=1),
|
|
desc=Path traversal in stp_diffs_csv aborts the service,
|
|
outds=work.test_results
|
|
)
|
|
|
|
/**
|
|
* dump results to the log for offline inspection
|
|
*/
|
|
data _null_;
|
|
set work.test_results;
|
|
putlog 'SECREVRESULT: ' test_result ' - ' test_description;
|
|
run;
|