fix: validate request inputs and add admin gates to public services
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
This commit is contained in:
@@ -23,7 +23,7 @@
|
||||
@li mp_abort.sas
|
||||
@li mf_getuniquename.sas
|
||||
@li mf_getuser.sas
|
||||
@li mf_verifymacvars.sas
|
||||
@li mp_validatecol.sas
|
||||
@li mpe_getgroups.sas
|
||||
|
||||
<h4> Related Macros </h4>
|
||||
@@ -52,10 +52,51 @@
|
||||
,msg=%str(outds should be a WORK table)
|
||||
)
|
||||
|
||||
/**
|
||||
* Validate inputs before they reach executable code. base_table is
|
||||
* interpolated into a SQL where clause (and callers may pass raw request
|
||||
* input), so it must be a well-formed LIBREF.DATASET and access_level
|
||||
* must be one of the known levels - anything else aborts before the
|
||||
* query is built. Values are read with symget (never re-resolved) and
|
||||
* scanned in a data step so no macro content in the input can execute.
|
||||
*/
|
||||
%local is_libds is_level;
|
||||
%let is_libds=0;
|
||||
%let is_level=0;
|
||||
data _null_;
|
||||
length _bt $64 _lvl $16;
|
||||
_bt=symget('base_table');
|
||||
_lvl=upcase(symget('access_level'));
|
||||
%mp_validatecol(_bt,LIBDS,is_libds)
|
||||
if is_libds=0 then do;
|
||||
call symputx('is_libds',0,'l');
|
||||
putlog 'ERR' 'OR: Invalid base_table:' _bt;
|
||||
stop;
|
||||
end;
|
||||
if _lvl not in ('EDIT','APPROVE','VIEW','SIGNOFF','AUDIT') then do;
|
||||
call symputx('is_level',0,'l');
|
||||
putlog 'ERR' 'OR: Invalid access_level:' _lvl;
|
||||
stop;
|
||||
end;
|
||||
/* escape any embedded quotes so the value cannot break out of the
|
||||
* double-quoted SQL literals below (defence in depth - the LIBDS
|
||||
* check above already rejects quotes) */
|
||||
_bt=tranwrd(_bt,'"','');
|
||||
call symputx('base_table',_bt,'l');
|
||||
call symputx('access_level',_lvl,'l');
|
||||
call symputx('is_libds',is_libds,'l');
|
||||
call symputx('is_level',1,'l');
|
||||
run;
|
||||
|
||||
%mp_abort(
|
||||
iftrue=(%mf_verifymacvars(base_table user access_level)=0)
|
||||
iftrue=(&is_libds ne 1)
|
||||
,mac=mpe_accesscheck
|
||||
,msg=%str(Missing base_table/user access_level variables)
|
||||
,msg=%str(Invalid base_table)
|
||||
)
|
||||
%mp_abort(
|
||||
iftrue=(&is_level ne 1)
|
||||
,mac=mpe_accesscheck
|
||||
,msg=%str(Invalid access_level)
|
||||
)
|
||||
|
||||
/* make unique temp table vars */
|
||||
|
||||
@@ -5,8 +5,13 @@
|
||||
|
||||
@li &parent= (parent path)
|
||||
|
||||
Requires membership of the DC administrators group.
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mf_getuser.sas
|
||||
@li mp_abort.sas
|
||||
@li mp_dirlist.sas
|
||||
@li mpe_getgroups.sas
|
||||
|
||||
@version 9.2
|
||||
@author 4GL Apps Ltd
|
||||
@@ -17,8 +22,37 @@
|
||||
**/
|
||||
|
||||
%global parent;
|
||||
/* if no flavour is specified, default to root */
|
||||
%let parent=%sysfunc(coalescec(&parent,/));
|
||||
%mpeinit()
|
||||
|
||||
/* check user is in admin group */
|
||||
%let cnt=0;
|
||||
%mpe_getgroups(user=%mf_getuser(),outds=work.usergroups)
|
||||
proc sql noprint;
|
||||
select count(*) into:cnt
|
||||
from usergroups
|
||||
where groupname="&mpeadmins";
|
||||
%mp_abort(iftrue= (&cnt=0)
|
||||
,mac=&_program
|
||||
,msg=%str(This service is only available to &mpeadmins members)
|
||||
)
|
||||
|
||||
/* if no parent is specified, default to root - read with symget
|
||||
* (never re-resolved) so macro content in the param cannot execute */
|
||||
%let is_bad=0;
|
||||
data _null_;
|
||||
length _parent $512;
|
||||
_parent=coalescec(symget('parent'),'/');
|
||||
if index(_parent,'%')>0 or index(_parent,'&')>0 then do;
|
||||
putlog 'ERR' 'OR: Invalid parent:' _parent;
|
||||
call symputx('is_bad',1,'l');
|
||||
end;
|
||||
else call symputx('parent',_parent,'g');
|
||||
run;
|
||||
|
||||
%mp_abort(iftrue=(&is_bad=1)
|
||||
,mac=&_program..sas
|
||||
,msg=%str(Invalid parent)
|
||||
)
|
||||
|
||||
%mp_dirlist(path=&parent,outds=dirlist, maxdepth=2)
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
@file
|
||||
@brief testing admin dirlist service - admin gate (security)
|
||||
@details The service requires membership of the DC administrators
|
||||
group. The test suite runs as a member of that group, so the gate
|
||||
passes and the directory listing is returned. (A non-admin negative
|
||||
test would need a second user, which this suite does not have.)
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mp_assert.sas
|
||||
@li mx_execute.sas
|
||||
|
||||
**/
|
||||
|
||||
%let _program=&appLoc/services/admin/dirlist;
|
||||
|
||||
data work.params;
|
||||
length name $32 value $1000;
|
||||
name='parent';value='/tmp';
|
||||
run;
|
||||
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputparams=work.params,
|
||||
outlib=web1
|
||||
)
|
||||
|
||||
%let nobs=0;
|
||||
proc sql noprint;
|
||||
select count(*) into: nobs from web1.dirlist;
|
||||
quit;
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&nobs>0),
|
||||
desc=Admin user gets a directory listing,
|
||||
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;
|
||||
@@ -15,6 +15,7 @@
|
||||
@li mp_ds2csv.sas
|
||||
@li mp_streamfile.sas
|
||||
@li mp_validatecol.sas
|
||||
@li mpe_getgroups.sas
|
||||
|
||||
@author 4GL Apps Ltd
|
||||
@copyright 4GL Apps Ltd. This code may only be used within Data Controller
|
||||
@@ -26,6 +27,18 @@
|
||||
%global dclib islib newlib;
|
||||
%mpeinit()
|
||||
|
||||
/* check user is in admin group */
|
||||
%let cnt=0;
|
||||
%mpe_getgroups(user=%mf_getuser(),outds=work.usergroups)
|
||||
proc sql noprint;
|
||||
select count(*) into:cnt
|
||||
from usergroups
|
||||
where groupname="&mpeadmins";
|
||||
%mp_abort(iftrue= (&cnt=0)
|
||||
,mac=&_program
|
||||
,msg=%str(The DC configuration can only be exported by &mpeadmins members)
|
||||
)
|
||||
|
||||
data _null_;
|
||||
newlib=coalescec(symget('dclib'),"&mpelib");
|
||||
%mp_validatecol(newlib,ISLIB,islib)
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
@file refreshcatalog.sas
|
||||
@brief Refreshes the library data catalog
|
||||
@details A library may be passed in a LIBREF url param.
|
||||
Requires membership of the DC administrators group.
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mpeinit.sas
|
||||
@li dc_refreshcatalog.sas
|
||||
@li mpe_getgroups.sas
|
||||
@li mp_abort.sas
|
||||
@li mp_validatecol.sas
|
||||
@li mpeterm.sas
|
||||
|
||||
@version 9.3
|
||||
@@ -18,6 +22,44 @@
|
||||
%global libref;
|
||||
%mpeinit()
|
||||
|
||||
/**
|
||||
* libref is a request input used in dc_assignlib and catalog queries -
|
||||
* it must be a well-formed libref before use. Read with symget (never
|
||||
* re-resolved) and validated in a data step.
|
||||
*/
|
||||
%let is_lib=0;
|
||||
data _null_;
|
||||
length _libref $8;
|
||||
_libref=coalescec(symget('libref'),'');
|
||||
/* an absent libref is a valid, full catalog refresh */
|
||||
if missing(_libref) then do;
|
||||
call symputx('is_lib',1,'l');
|
||||
call symputx('libref','','g');
|
||||
stop;
|
||||
end;
|
||||
%mp_validatecol(_libref,ISLIB,is_lib)
|
||||
if is_lib=0 then putlog 'ERR' 'OR: Invalid libref:' _libref;
|
||||
call symputx('is_lib',is_lib,'l');
|
||||
if is_lib=1 then call symputx('libref',upcase(_libref),'g');
|
||||
run;
|
||||
|
||||
%mp_abort(iftrue= (&is_lib ne 1)
|
||||
,mac=&_program..sas
|
||||
,msg=%str(Invalid libref)
|
||||
)
|
||||
|
||||
/* check user is in admin group */
|
||||
%let cnt=0;
|
||||
%mpe_getgroups(user=%mf_getuser(),outds=work.usergroups)
|
||||
proc sql noprint;
|
||||
select count(*) into:cnt
|
||||
from usergroups
|
||||
where groupname="&mpeadmins";
|
||||
%mp_abort(iftrue= (&cnt=0)
|
||||
,mac=&_program
|
||||
,msg=%str(This service is only available to &mpeadmins members)
|
||||
)
|
||||
|
||||
%dc_refreshcatalog(&libref)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
@file
|
||||
@brief testing admin refreshcatalog service - admin gate + libref validation (security)
|
||||
@details The service requires membership of the DC administrators
|
||||
group and a well-formed libref (or none). An invalid libref aborts
|
||||
the service, which shows up as a canceled child job (an aborted
|
||||
service registers no webout). The test suite runs as a member of
|
||||
the admin group, so the gate passes here.
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mp_assert.sas
|
||||
@li mx_execute.sas
|
||||
|
||||
**/
|
||||
|
||||
%let _program=&appLoc/services/admin/refreshcatalog;
|
||||
|
||||
/**
|
||||
* Test 1 - an invalid libref must abort the service
|
||||
*/
|
||||
data work.params1;
|
||||
length name $32 value $1000;
|
||||
name='libref';value='A.%sysevalf(3+4)B';output;
|
||||
run;
|
||||
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputparams=work.params1,
|
||||
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=Macro content in libref aborts the service,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/**
|
||||
* Test 2 - a valid libref still refreshes the catalog (admin user)
|
||||
*/
|
||||
data work.params2;
|
||||
length name $32 value $1000;
|
||||
name='libref';value='DCTEST';output;
|
||||
run;
|
||||
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputparams=work.params2,
|
||||
outlib=web2
|
||||
)
|
||||
|
||||
%let msgcheck=0;
|
||||
data _null_;
|
||||
set web2.sasparams;
|
||||
putlog (_all_)(=);
|
||||
if index(msg,'Catalog Refresh Complete') then call symputx('msgcheck',1);
|
||||
run;
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&msgcheck=1),
|
||||
desc=Valid libref refresh completes for admin user,
|
||||
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;
|
||||
@@ -1,11 +1,13 @@
|
||||
/**
|
||||
@file refreshlibs.sas
|
||||
@brief Refreshes the library data catalog
|
||||
@details
|
||||
@details Requires membership of the DC administrators group.
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mpeinit.sas
|
||||
@li mpe_refreshlibs.sas
|
||||
@li mpe_getgroups.sas
|
||||
@li mp_abort.sas
|
||||
|
||||
@version 9.3
|
||||
@author 4GL Apps Ltd
|
||||
@@ -17,4 +19,16 @@
|
||||
|
||||
%mpeinit()
|
||||
|
||||
/* check user is in admin group */
|
||||
%let cnt=0;
|
||||
%mpe_getgroups(user=%mf_getuser(),outds=work.usergroups)
|
||||
proc sql noprint;
|
||||
select count(*) into:cnt
|
||||
from usergroups
|
||||
where groupname="&mpeadmins";
|
||||
%mp_abort(iftrue= (&cnt=0)
|
||||
,mac=&_program
|
||||
,msg=%str(This service is only available to &mpeadmins members)
|
||||
)
|
||||
|
||||
%mpe_refreshlibs()
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
@li mp_abort.sas
|
||||
@li mp_binarycopy.sas
|
||||
@li mp_streamfile.sas
|
||||
@li mp_validatecol.sas
|
||||
|
||||
@version 9.2
|
||||
@author 4GL Apps Ltd
|
||||
@@ -22,6 +23,52 @@
|
||||
%mpeinit()
|
||||
%mpe_getvars(BrowserParams, BrowserParams);
|
||||
|
||||
/**
|
||||
* Validate inputs before they reach executable code. libds is passed to
|
||||
* the access check (which interpolates it into SQL) and table is used in
|
||||
* the staging file path, so both must be well formed before use. Values
|
||||
* are re-read with symget (never re-resolved) and validated in a data
|
||||
* step so no macro content in the request can execute.
|
||||
*/
|
||||
%let is_libds=0;
|
||||
%let is_table=0;
|
||||
%let is_csv=0;
|
||||
data _null_;
|
||||
length _libds $64 _table $64 _csv $128;
|
||||
_libds=symget('libds');
|
||||
_table=symget('table');
|
||||
_csv=coalescec(symget('stp_diffs_csv'),'tempDiffs.csv');
|
||||
%mp_validatecol(_libds,LIBDS,is_libds)
|
||||
%mp_validatecol(_table,ISNAME,is_table)
|
||||
/* the diffs csv filename must stay inside the staging directory */
|
||||
if findc(_csv,'/\')>0 or index(_csv,'..')>0 then do;
|
||||
is_csv=0;
|
||||
putlog 'ERR' 'OR: Invalid stp_diffs_csv:' _csv;
|
||||
end;
|
||||
else is_csv=1;
|
||||
if is_libds=0 then putlog 'ERR' 'OR: Invalid libds:' _libds;
|
||||
if is_table=0 then putlog 'ERR' 'OR: Invalid table:' _table;
|
||||
call symputx('is_libds',is_libds,'l');
|
||||
call symputx('is_table',is_table,'l');
|
||||
call symputx('is_csv',is_csv,'l');
|
||||
if is_libds=1 then call symputx('libds',_libds,'g');
|
||||
if is_table=1 then call symputx('table',_table,'g');
|
||||
if is_csv=1 then call symputx('stp_diffs_csv',_csv,'g');
|
||||
run;
|
||||
|
||||
%mp_abort(iftrue= (&is_libds ne 1)
|
||||
,mac=&_program..sas
|
||||
,msg=%str(Invalid libds)
|
||||
)
|
||||
%mp_abort(iftrue= (&is_table ne 1)
|
||||
,mac=&_program..sas
|
||||
,msg=%str(Invalid table)
|
||||
)
|
||||
%mp_abort(iftrue= (&is_csv ne 1)
|
||||
,mac=&_program..sas
|
||||
,msg=%str(Invalid stp_diffs_csv)
|
||||
)
|
||||
|
||||
/* security checks */
|
||||
%let user=%mf_getuser();
|
||||
%mpe_accesscheck(&libds,outds=authEDIT,user=&user,access_level=EDIT)
|
||||
@@ -51,5 +98,4 @@
|
||||
%mpestp_diffs()
|
||||
|
||||
|
||||
|
||||
%mpeterm()
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
@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;
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li dc_assignlib.sas
|
||||
@li mf_getvalue.sas
|
||||
@li mp_abort.sas
|
||||
@li mp_validatecol.sas
|
||||
|
||||
@version 9.2
|
||||
@author 4GL Apps Ltd
|
||||
@@ -18,7 +18,29 @@
|
||||
%mpeinit()
|
||||
|
||||
|
||||
%let ds=%mf_getvalue(work.iwant,libds);
|
||||
/**
|
||||
* The libds is read from the IWANT input table. Reading it with
|
||||
* mf_getvalue would re-resolve any macro content in the value, so it is
|
||||
* read with symget in a data step and validated (LIBREF.DATASET) before
|
||||
* it is used in proc contents.
|
||||
*/
|
||||
%let is_libds=0;
|
||||
data _null_;
|
||||
length _libds $64;
|
||||
set work.iwant;
|
||||
_libds=libds;
|
||||
%mp_validatecol(_libds,LIBDS,is_libds)
|
||||
if is_libds=0 then putlog 'ERR' 'OR: Invalid libds:' _libds;
|
||||
call symputx('is_libds',is_libds,'l');
|
||||
if is_libds=1 then call symputx('ds',upcase(_libds),'l');
|
||||
stop;
|
||||
run;
|
||||
|
||||
%mp_abort(iftrue= (&is_libds ne 1)
|
||||
,mac=&_program..sas
|
||||
,msg=%str(Invalid libds)
|
||||
)
|
||||
|
||||
%dc_assignlib(READ,%scan(&ds,1,.))
|
||||
|
||||
proc contents noprint data=&ds
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
@file
|
||||
@brief testing getcols service - input validation (security)
|
||||
@details The libds in the IWANT input table must be a well-formed
|
||||
LIBREF.DATASET. An invalid value aborts the service before it
|
||||
reaches proc contents. The abort shows up as a canceled child job
|
||||
(an aborted service registers no webout).
|
||||
|
||||
The payload in test 1 resolves to a REAL table when the request
|
||||
content is executed as macro code, so on a vulnerable service the
|
||||
job completes, and only the validating service cancels it - the
|
||||
assertion 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/public/getcols;
|
||||
|
||||
/**
|
||||
* Test 1 - macro content in libds must abort the service
|
||||
*/
|
||||
%let f1=%mf_getuniquefileref();
|
||||
data _null_;
|
||||
file &f1 termstr=crlf;
|
||||
put 'LIBDS:$41.';
|
||||
put '%sysfunc(coalescec(&dclib..MPE_X_TEST,))';
|
||||
run;
|
||||
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputfiles=&f1:iwant,
|
||||
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=Macro content in libds aborts the service,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/**
|
||||
* Test 2 - valid libds still returns columns
|
||||
*/
|
||||
%let f2=%mf_getuniquefileref();
|
||||
data _null_;
|
||||
file &f2 termstr=crlf;
|
||||
put 'LIBDS:$41.';
|
||||
put "&dclib..MPE_X_TEST";
|
||||
run;
|
||||
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputfiles=&f2:iwant,
|
||||
outlib=web2
|
||||
)
|
||||
|
||||
%let nobs=0;
|
||||
proc sql noprint;
|
||||
select count(*) into: nobs from web2.cols;
|
||||
quit;
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&nobs>0),
|
||||
desc=Valid libds returns columns,
|
||||
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;
|
||||
@@ -35,7 +35,6 @@
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mf_existds.sas
|
||||
@li mf_getvalue.sas
|
||||
@li mf_verifymacvars.sas
|
||||
@li dc_assignlib.sas
|
||||
@li mf_getvarformat.sas
|
||||
@@ -43,6 +42,7 @@
|
||||
@li mp_cntlout.sas
|
||||
@li mp_filtercheck.sas
|
||||
@li mp_filtergenerate.sas
|
||||
@li mp_validatecol.sas
|
||||
|
||||
@version 9.2
|
||||
@author 4GL Apps Ltd.
|
||||
@@ -77,18 +77,53 @@ data _null_;
|
||||
put (_all_)(=);
|
||||
run;
|
||||
|
||||
%let libds=%mf_getvalue(work.iwant,libds);
|
||||
%let col2=%mf_getvalue(work.iwant,col);
|
||||
/**
|
||||
* libds and col are request inputs that flow into executable positions
|
||||
* (set &libds, proc sql select &col2). They are read from the IWANT
|
||||
* table with symget in a data step (never re-resolved) and validated
|
||||
* here before use - mf_getvalue would re-resolve any macro content in
|
||||
* the value before this code ran.
|
||||
*/
|
||||
%let libds=;
|
||||
%let col2=;
|
||||
%let is_libds=0;
|
||||
%let is_col=0;
|
||||
data _null_;
|
||||
length _libds $64 _col $32 _ds $34;
|
||||
set work.iwant;
|
||||
_libds=libds;
|
||||
_col=col;
|
||||
%mp_validatecol(_libds,LIBDS,is_libds)
|
||||
/* permit the format-catalog form: LIBREF.CATALOGNAME-FC */
|
||||
if is_libds=0 then do;
|
||||
_ds=scan(_libds,1,'-');
|
||||
%mp_validatecol(_ds,LIBDS,is_libds)
|
||||
end;
|
||||
%mp_validatecol(_col,ISNAME,is_col)
|
||||
if is_libds=0 then putlog 'ERR' 'OR: Invalid libds:' _libds;
|
||||
if is_col=0 then putlog 'ERR' 'OR: Invalid col:' _col;
|
||||
call symputx('is_libds',is_libds,'l');
|
||||
call symputx('is_col',is_col,'l');
|
||||
if is_libds=1 then call symputx('libds',upcase(_libds),'l');
|
||||
if is_col=1 then call symputx('col2',upcase(_col),'l');
|
||||
stop;
|
||||
run;
|
||||
|
||||
%let is_fmt=0;
|
||||
%let startrow=1;
|
||||
%let rows=4000;
|
||||
|
||||
%put &=libds;
|
||||
%put &=col2;
|
||||
|
||||
%mp_abort(iftrue= (%mf_verifymacvars(libds col2)=0)
|
||||
,mac=&_program..sas
|
||||
,msg=%str(Missing inputs from iwant. Libds=&libds col=&col2 )
|
||||
,msg=%str(Missing inputs from iwant)
|
||||
)
|
||||
%mp_abort(iftrue= (&is_libds ne 1)
|
||||
,mac=&_program..sas
|
||||
,msg=%str(Invalid libds)
|
||||
)
|
||||
%mp_abort(iftrue= (&is_col ne 1)
|
||||
,mac=&_program..sas
|
||||
,msg=%str(Invalid col)
|
||||
)
|
||||
|
||||
%dc_assignlib(WRITE,%scan(&libds,1,.))
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
@file
|
||||
@brief testing getcolvals service - input validation (security)
|
||||
@details The libds and col in the IWANT input table must be
|
||||
well-formed (LIBREF.DATASET and SAS name). An invalid value aborts
|
||||
the service, which shows up as a canceled child job (an aborted
|
||||
service registers no webout).
|
||||
|
||||
The payloads in tests 1-2 resolve to a REAL table / column when the
|
||||
request content is executed as macro code, so on a vulnerable
|
||||
service the job completes, and only the validating service cancels
|
||||
it - the assertion 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/public/getcolvals;
|
||||
|
||||
/**
|
||||
* Test 1 - macro content in libds must abort the service
|
||||
*/
|
||||
%let f1=%mf_getuniquefileref();
|
||||
data _null_;
|
||||
file &f1 termstr=crlf;
|
||||
put 'LIBDS:$19. COL:$9.';
|
||||
put '%sysfunc(coalescec(&dclib..MPE_X_TEST,)),SOME_TIME';
|
||||
run;
|
||||
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputfiles=&f1:iwant,
|
||||
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=Macro content in libds aborts the service,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/**
|
||||
* Test 2 - macro content in col must abort the service
|
||||
*/
|
||||
%let f2=%mf_getuniquefileref();
|
||||
data _null_;
|
||||
file &f2 termstr=crlf;
|
||||
put 'LIBDS:$19. COL:$9.';
|
||||
put '&dclib..MPE_X_TEST,%sysfunc(coalescec(SOME_TIME,))';
|
||||
run;
|
||||
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputfiles=&f2:iwant,
|
||||
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=Macro content in col aborts the service,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/**
|
||||
* Test 3 - valid inputs still return values
|
||||
*/
|
||||
%let f3=%mf_getuniquefileref();
|
||||
data _null_;
|
||||
file &f3 termstr=crlf;
|
||||
put 'LIBDS:$19. COL:$9.';
|
||||
put "&dclib..MPE_X_TEST,SOME_TIME";
|
||||
run;
|
||||
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputfiles=&f3:iwant,
|
||||
outlib=web3
|
||||
)
|
||||
|
||||
%let nobs=0;
|
||||
proc sql noprint;
|
||||
select count(*) into: nobs from web3.vals;
|
||||
quit;
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&nobs>0),
|
||||
desc=Valid inputs return values,
|
||||
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;
|
||||
@@ -11,16 +11,16 @@
|
||||
@li filter - the filter RK if used
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mf_verifymacvars.sas
|
||||
@li mf_getuser.sas
|
||||
@li mf_existfeature.sas
|
||||
@li dc_assignlib.sas
|
||||
@li mp_ds2cards.sas
|
||||
@li mp_ds2csv.sas
|
||||
@li mp_abort.sas
|
||||
@li mp_binarycopy.sas
|
||||
@li mp_cntlout.sas
|
||||
@li mp_ds2cards.sas
|
||||
@li mp_ds2csv.sas
|
||||
@li mp_streamfile.sas
|
||||
@li mp_validatecol.sas
|
||||
@li mpe_filtermaster.sas
|
||||
|
||||
|
||||
@@ -37,9 +37,62 @@
|
||||
%let user=%mf_getuser();
|
||||
%let is_fmt=0;
|
||||
|
||||
%mp_abort(iftrue= (%mf_verifymacvars(type table)=0)
|
||||
/**
|
||||
* Validate inputs before they reach executable code. table is used as a
|
||||
* dataset reference, in the output file path, and in the download filename,
|
||||
* so it must be a well-formed LIBREF.DATASET (the trailing -FC catalog
|
||||
* suffix is permitted); filter must be an integer. Values are read with
|
||||
* symget (never re-resolved) and validated in a data step so no macro
|
||||
* content in the input can execute.
|
||||
*/
|
||||
%let is_libds=0;
|
||||
%let is_int=0;
|
||||
%let is_type=0;
|
||||
data _null_;
|
||||
length _table $64 _filter $16 _ds $34 _type $16;
|
||||
_type=upcase(coalescec(symget('type'),''));
|
||||
_table=coalescec(symget('table'),'');
|
||||
_filter=coalescec(symget('filter'),'0');
|
||||
if missing(_table) then do;
|
||||
putlog 'ERR' 'OR: Missing table';
|
||||
stop;
|
||||
end;
|
||||
%mp_validatecol(_table,LIBDS,is_libds)
|
||||
/* permit the format-catalog form: LIBREF.CATALOGNAME-FC */
|
||||
if is_libds=0 then do;
|
||||
_ds=scan(_table,1,'-');
|
||||
%mp_validatecol(_ds,LIBDS,is_libds)
|
||||
end;
|
||||
/* an absent filter is a valid, unfiltered download */
|
||||
if missing(_filter) then _filter='0';
|
||||
%mp_validatecol(_filter,ISINT,is_int)
|
||||
/* type is validated against a fixed list of download formats */
|
||||
length _types_ok 8;
|
||||
_types_ok=0;
|
||||
if _type in ('SAS','CSV','EXCEL','MARKDOWN','WEBCSV','WEBTAB')
|
||||
then _types_ok=1;
|
||||
else putlog 'ERR' 'OR: Invalid type:' _type;
|
||||
if is_libds=0 then putlog 'ERR' 'OR: Invalid table:' _table;
|
||||
if is_int=0 then putlog 'ERR' 'OR: Invalid filter:' _filter;
|
||||
call symputx('is_libds',is_libds,'l');
|
||||
call symputx('is_int',is_int,'l');
|
||||
call symputx('is_type',_types_ok,'l');
|
||||
call symputx('filter',_filter,'l');
|
||||
if is_libds=1 then call symputx('table',upcase(_table),'l');
|
||||
if _types_ok=1 then call symputx('type',_type,'l');
|
||||
run;
|
||||
|
||||
%mp_abort(iftrue= (&is_libds ne 1)
|
||||
,mac=&_program..sas
|
||||
,msg=%str(Invalid inputs: type table)
|
||||
,msg=%str(Invalid table)
|
||||
)
|
||||
%mp_abort(iftrue= (&is_int ne 1)
|
||||
,mac=&_program..sas
|
||||
,msg=%str(Invalid filter)
|
||||
)
|
||||
%mp_abort(iftrue= (&is_type ne 1)
|
||||
,mac=&_program..sas
|
||||
,msg=%str(Invalid type)
|
||||
)
|
||||
|
||||
%let libds=%upcase(&table); /* actual source */
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
@file
|
||||
@brief testing getrawdata service - input validation (security)
|
||||
@details table must be a well-formed LIBREF.DATASET (the trailing
|
||||
format-catalog suffix is permitted), filter must be an integer and
|
||||
type one of the supported download types. An invalid value aborts
|
||||
the service before any request content can execute. The abort is
|
||||
asserted from the child job state: the payloads either resolve to a
|
||||
real table when executed as macro code, or write a file outside the
|
||||
WORK directory (verified by live probe) - so on the vulnerable
|
||||
service the job completes, and only the validating service cancels
|
||||
it. The assertion cannot pass against a service that does not
|
||||
validate.
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mp_assert.sas
|
||||
@li mx_execute.sas
|
||||
|
||||
**/
|
||||
|
||||
%let _program=&appLoc/services/public/getrawdata;
|
||||
|
||||
/**
|
||||
* Test 1 - macro content in table must abort the service
|
||||
* (the payload resolves to a real table when executed as macro code)
|
||||
*/
|
||||
data work.params1;
|
||||
length name $32 value $1000;
|
||||
name='type';value='CSV';output;
|
||||
name='table';value='%sysfunc(coalescec(&mpelib..MPE_X_TEST,))';output;
|
||||
name='filter';value='0';output;
|
||||
run;
|
||||
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputparams=work.params1,
|
||||
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=Macro content in table aborts the service,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/**
|
||||
* Test 2 - sql injection in filter must abort the service
|
||||
*/
|
||||
data work.params2;
|
||||
length name $32 value $1000;
|
||||
name='type';value='CSV';output;
|
||||
name='table';value="&dclib..MPE_X_TEST";output;
|
||||
name='filter';value='0 or 1=1';output;
|
||||
run;
|
||||
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputparams=work.params2,
|
||||
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=SQL injection in filter aborts the service,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/**
|
||||
* Test 3 - path traversal in table must abort the service
|
||||
* (verified by live probe: on a vulnerable service this writes
|
||||
* ../SECPROBE.csv outside the WORK directory and completes)
|
||||
*/
|
||||
data work.params3;
|
||||
length name $32 value $1000;
|
||||
name='type';value='CSV';output;
|
||||
name='table';value='../secprobe';output;
|
||||
name='filter';value='0';output;
|
||||
run;
|
||||
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputparams=work.params3,
|
||||
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 table aborts the service,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/**
|
||||
* Test 4 - the valid request must still work (positive control)
|
||||
*/
|
||||
data work.params4;
|
||||
length name $32 value $1000;
|
||||
name='type';value='CSV';output;
|
||||
name='table';value="&dclib..MPE_X_TEST";output;
|
||||
name='filter';value='0';output;
|
||||
run;
|
||||
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputparams=work.params4,
|
||||
outref=web4,
|
||||
viyaresult=WEBOUT_TXT
|
||||
)
|
||||
|
||||
%let ok4=0;
|
||||
data _null_;
|
||||
infile web4;
|
||||
input;
|
||||
if _infile_=:'PRIMARY_KEY_FIELD' then do;
|
||||
call symputx('ok4',1);
|
||||
stop;
|
||||
end;
|
||||
run;
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&ok4=1),
|
||||
desc=Valid table request still returns data,
|
||||
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;
|
||||
@@ -26,8 +26,8 @@
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li dc_assignlib.sas
|
||||
@li mf_getvalue.sas
|
||||
@li mp_filterstore.sas
|
||||
@li mp_validatecol.sas
|
||||
@li removecolsfromwork.sas
|
||||
|
||||
@version 9.2
|
||||
@@ -40,7 +40,31 @@
|
||||
|
||||
%mpeinit()
|
||||
|
||||
%let ds=%upcase(%mf_getvalue(work.iwant,filter_table));
|
||||
/**
|
||||
* filter_table is a request input that flows into executable positions
|
||||
* (mp_filterstore libds=, which interpolates it into SQL). It is read
|
||||
* from the IWANT table in a data step (never re-resolved) and validated
|
||||
* before use - mf_getvalue would re-resolve any macro content in the
|
||||
* value before this code ran.
|
||||
*/
|
||||
%let ds=;
|
||||
%let is_libds=0;
|
||||
data _null_;
|
||||
length _ds $64;
|
||||
set work.iwant;
|
||||
_ds=filter_table;
|
||||
%mp_validatecol(_ds,LIBDS,is_libds)
|
||||
if is_libds=0 then putlog 'ERR' 'OR: Invalid filter_table:' _ds;
|
||||
call symputx('is_libds',is_libds,'l');
|
||||
if is_libds=1 then call symputx('ds',upcase(_ds),'l');
|
||||
stop;
|
||||
run;
|
||||
|
||||
%mp_abort(iftrue= (&is_libds ne 1)
|
||||
,mac=&_program..sas
|
||||
,msg=%str(Invalid filter_table)
|
||||
)
|
||||
|
||||
%dc_assignlib(WRITE,%scan(&ds,1,.))
|
||||
|
||||
%mp_filterstore(
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
@file
|
||||
@brief testing validatefilter service - input validation (security)
|
||||
@details The filter_table in the IWANT input table must be a
|
||||
well-formed LIBREF.DATASET. An invalid value aborts the service
|
||||
before it reaches mp_filterstore. The abort shows up as a canceled
|
||||
child job (an aborted service registers no webout).
|
||||
|
||||
The payload in test 1 resolves to a REAL table when the request
|
||||
content is executed as macro code, so on a vulnerable service the
|
||||
job completes, and only the validating service cancels it - the
|
||||
assertion 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/public/validatefilter;
|
||||
|
||||
/**
|
||||
* Test 1 - macro content in filter_table must abort the service
|
||||
*/
|
||||
%let f1=%mf_getuniquefileref();
|
||||
data _null_;
|
||||
file &f1 termstr=crlf;
|
||||
put 'FILTER_TABLE:$41.';
|
||||
put '%sysfunc(coalescec(&dclib..MPE_TABLES,))';
|
||||
run;
|
||||
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputfiles=&f1:iwant,
|
||||
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=Macro content in filter_table aborts the service,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/**
|
||||
* Test 2 - valid filter_table still stores a filter
|
||||
*/
|
||||
%let f2=%mf_getuniquefileref();
|
||||
data _null_;
|
||||
file &f2 termstr=crlf;
|
||||
put 'FILTER_TABLE:$41.';
|
||||
put "&dclib..MPE_TABLES";
|
||||
run;
|
||||
%let f3=%mf_getuniquefileref();
|
||||
data _null_;
|
||||
file &f3 termstr=crlf;
|
||||
infile datalines4 dsd;
|
||||
input;
|
||||
put _infile_;
|
||||
datalines4;
|
||||
GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32. OPERATOR_NM:$10. RAW_VALUE:$4000.
|
||||
AND,AND,1,LIBREF,CONTAINS,"'DC'"
|
||||
AND,OR,2,DSN,=,"'MPE_LOCK_ANYTABLE'"
|
||||
;;;;
|
||||
run;
|
||||
|
||||
%mx_execute(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputfiles=&f2:iwant &f3:filterquery,
|
||||
outlib=web2
|
||||
)
|
||||
|
||||
%let nobs=0;
|
||||
proc sql noprint;
|
||||
select count(*) into: nobs from web2.result;
|
||||
quit;
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&nobs>0),
|
||||
desc=Valid filter_table returns a filter result,
|
||||
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;
|
||||
Reference in New Issue
Block a user