From d60981c9df7b446b3b608f74706a385ee7a7ee70 Mon Sep 17 00:00:00 2001 From: netroms Date: Tue, 15 Oct 2024 03:10:57 +0800 Subject: [PATCH] feat: add more validation to file upload (#18112) (#18814) [DHIS2-17311] --- .../fileresource/FileResourceBlocklist.java | 8 +- .../FileResourceControllerTest.java | 21 ++- .../src/test/resources/file/dhis2.png | Bin 0 -> 11417 bytes dhis-2/dhis-web-api/pom.xml | 6 + .../controller/FileResourceController.java | 22 ++- .../dhis/webapi/utils/FileResourceUtils.java | 173 +++++++++++++++++- 6 files changed, 207 insertions(+), 23 deletions(-) create mode 100644 dhis-2/dhis-test-web-api/src/test/resources/file/dhis2.png diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/fileresource/FileResourceBlocklist.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/fileresource/FileResourceBlocklist.java index 257c445a0540..e5c75c41fddd 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/fileresource/FileResourceBlocklist.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/fileresource/FileResourceBlocklist.java @@ -34,7 +34,7 @@ * @author Lars Helge Overland */ public class FileResourceBlocklist { - private static final ImmutableSet CONTENT_TYPES = + private static final ImmutableSet BLOCKED_CONTENT_TYPES = ImmutableSet.of( // Web "text/html", @@ -54,7 +54,7 @@ public class FileResourceBlocklist { "application/x-sh", "application/x-csh"); - private static final ImmutableSet FILE_EXTENSIONS = + private static final ImmutableSet BLOCKED_FILE_EXTENSIONS = ImmutableSet.of( // Web "html", @@ -93,11 +93,11 @@ public static boolean isValid(FileResource fileResource) { return false; } - if (CONTENT_TYPES.contains(fileResource.getContentType().toLowerCase())) { + if (BLOCKED_CONTENT_TYPES.contains(fileResource.getContentType().toLowerCase())) { return false; } - if (FILE_EXTENSIONS.contains( + if (BLOCKED_FILE_EXTENSIONS.contains( FilenameUtils.getExtension(fileResource.getName().toLowerCase()))) { return false; } diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/FileResourceControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/FileResourceControllerTest.java index db17a8d84ac1..b21539852d04 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/FileResourceControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/FileResourceControllerTest.java @@ -29,21 +29,26 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; import org.hisp.dhis.feedback.ErrorCode; import org.hisp.dhis.jsontree.JsonObject; import org.hisp.dhis.web.HttpStatus; import org.hisp.dhis.webapi.DhisControllerConvenienceTest; import org.hisp.dhis.webapi.json.domain.JsonWebMessage; import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; import org.springframework.mock.web.MockMultipartFile; class FileResourceControllerTest extends DhisControllerConvenienceTest { @Test - void testSaveOrgUnitImage() { + void testSaveOrgUnitImage() throws IOException { + File file = new ClassPathResource("file/dhis2.png").getFile(); MockMultipartFile image = new MockMultipartFile( - "file", "OU_profile_image.png", "image/png", "<>".getBytes()); + "file", "OU_profile_image.png", "image/png", Files.readAllBytes(file.toPath())); HttpResponse response = POST_MULTIPART("/fileResources?domain=ORG_UNIT", image); JsonObject savedObject = response.content(HttpStatus.ACCEPTED).getObject("response").getObject("fileResource"); @@ -51,10 +56,11 @@ void testSaveOrgUnitImage() { } @Test - void testSaveOrgUnitImageWithUid() { + void testSaveOrgUnitImageWithUid() throws IOException { + File file = new ClassPathResource("file/dhis2.png").getFile(); MockMultipartFile image = new MockMultipartFile( - "file", "OU_profile_image.png", "image/png", "<>".getBytes()); + "file", "OU_profile_image.png", "image/png", Files.readAllBytes(file.toPath())); HttpResponse response = POST_MULTIPART("/fileResources?domain=ORG_UNIT&uid=0123456789a", image); JsonObject savedObject = response.content(HttpStatus.ACCEPTED).getObject("response").getObject("fileResource"); @@ -63,10 +69,11 @@ void testSaveOrgUnitImageWithUid() { } @Test - void testSaveOrgUnitImageWithUid_Update() { + void testSaveOrgUnitImageWithUid_Update() throws IOException { + File file = new ClassPathResource("file/dhis2.png").getFile(); MockMultipartFile image = new MockMultipartFile( - "file", "OU_profile_image.png", "image/png", "<>".getBytes()); + "file", "OU_profile_image.png", "image/png", Files.readAllBytes(file.toPath())); HttpResponse response = POST_MULTIPART("/fileResources?domain=ORG_UNIT&uid=0123456789x", image); JsonObject savedObject = response.content(HttpStatus.ACCEPTED).getObject("response").getObject("fileResource"); @@ -76,7 +83,7 @@ void testSaveOrgUnitImageWithUid_Update() { // now update the resource with a different image but the same UID MockMultipartFile image2 = new MockMultipartFile( - "file", "OU_profile_image2.png", "image/png", "<>".getBytes()); + "file", "OU_profile_image2.png", "image/png", Files.readAllBytes(file.toPath())); JsonWebMessage message = assertWebMessage( diff --git a/dhis-2/dhis-test-web-api/src/test/resources/file/dhis2.png b/dhis-2/dhis-test-web-api/src/test/resources/file/dhis2.png new file mode 100644 index 0000000000000000000000000000000000000000..73f41ec54212864c380449218cf6413decfb5593 GIT binary patch literal 11417 zcmd^lcQjnl_pek*gb+qAK|&BjnCPS^K@e>WV#FXAEqX7J=q-re5~9uw2BVi?^j>E4 z-ohwR$Ea`e`@Z+jd+V+D-rw)8v(DPtn*q5k%3 zzuV9T3bRHE^F~UGMk=L%dmo!{bsOfX4D8`B=xqW*1}}l z%=|8x*{+4zzJMCvgdx?vojS|5;_ zV=Jd4h6@zV<=n>YjCll(eB|2p*sY!4C`Q1e;|b)OkXNU$N$gY8I1y;PsBf2;S-hCp zcc6JZ(EPi2K)1xlL`lme>A)UYSjvmwzL)lCilP0=PML2a2Grbss6`EGd>htu&(ieB z){Gg{^3K(X8`kyC(e=&M`%(t-$pQHn== ztkgUdY5uLwGNQyXYYY}yY!ijDjjOiJ9=D4rvrA}n{8s81SK*jD>5@?CmR##rFy)bm z_DHJsNUrvP&wFOHdL>tSrPh0=)_TLIeSft1rPumr)CUyL2Ih8t`O)w-yZLMRQb=KU zNZDLy`FvPzOGM>jB)m7epf$RBF}n6oOvykjye+9Kq2W&=syDg3Ke=%^1=XEW zIg(o1p4PIF{(B{(q9+60`=hGwXZ?5%X7yLy@UQmu+}eTMuFd@4lLeiB3LA!tdUxQR ztBBS~ME5$f8H;S2E$!PV9o$8=V$0el%7^zWI;YV+OO=E8>h77E4qQ#wWKG{f4R*J- zcdmA1tFC9Nv2VI@Xr&3a-86aFJUG`nv4a_2XqzRpPyOx0E_IJDchBthOs@BiFZGTu z_s#DQOs)(}tqx8s4=)o(X8w#W?c?S*aDPt67x3e&hf_qoQ88?&q1b9lns z=IQ*}&cY^PasBV|#_sa=@ygEe+UDNcU*h^MabxEIzjv~^d$@gYva@%zb9l0QK-fJx z-90+nJ3QVaob4YG4+z9V!U^HzjCgu}a&~@3YUdZ{7yr2~FD@@GE-(MnS!_OVCnMw2 zS5bJW>oK`Cz_X%vC1=#_wVbaN%>ODX?gSJrwH%UXJNDa^^~6v}C3aLPYCYZ} zrl&j0@!s~7CnR|fK2+b^SUWpTA0S5GCVSQ5{+FKY0f_7cf-Dq3MlJA9#@zYuGW5oO zmr+;A{!0!3Um^Q1nFJ@v|AGI%sQ!oP|3&pbOeEI-Yt{eB$p0JF|IPgVH>v?_5qwx* zq*hda=q53tb`o#XS{K-ZC-so`>W!nU!=M1!}(K;mg07W?+t&$Ng`Ul8uli7PGM$9c{AZRg-Uda!nrDt5_BaxXMY4?sERv zJ9!AtU)6llGkA}QEA|b*DM;y5P<_W=?NOXjFk^W)k1OI5TWlb@I=cL!hY=N+%+g4d z-?H8DTtx+!Js}F-uhGsa7kM*m{aq`z>ai>!Dn@ohv~Hib#)F(jAhc}Vs2qmH@qPrY z6^1DC0hL0c*p%y)_*m$Z^kw%8PwV_7XT!i(3cAy0XD)P|^=97`zBj8N4*W<|L;E(n zxv9JHa)HSB@6YH|%GO(H4_Vc`>UlH@k7TalYA9F;3z~wZhXP(4pO5b4Gq~xlLUa+ln`WR;{F+;Znnr$=3WpB<;U`7zp-sL9THnWmdGW_%p$ng zD(5jDWObt2!tgot#=O1lnln;~5AR&~UUD(FSkY*8_A~fOHJ|NV=?AfM-IC>^Bnuf- z?v{5b;J5$8)_|asii=y?8?iVWN^cu^q^8tMWH0Ex|gTz8_6;fV>8* zYebpY%@D!TO)tCIZdb`rz=x-aIxy|OzGRWwuh?+oU?`s6vSl!a0<)KrEVD+y~ zZMUs11=3J~kNd}8JZ<#0+M{vNWrrK4H04L6&z&$r!tc?JUA+<~unec*^R zpXLHnxqqXU1BIVsm9RHyebLx_!fed$u>Aq1L*6>0Po(|Fm^tnsCv8zG5l1yN0#M3e z@Qef15kKTt)w%eQc)XaBk=4_JG=c>;2*Z2~CU?ND;j=4SFYU{p=^Ab!^=jr_c%X`i z&(T0s#Dawf?}(=cp_#~k#R$EDW=?p;8FP~OzEHR0jvHqT1Z{4@Pr_b_Mjwlf;3bq+ z1U6(OO!(iyK^dw-VX96RfbR)rvV!btOb7uoPwg(T6R` z`Yy6-$K}o4s}Gk41#*Xv>%%8kEsO~4^3JQZ`WF+*bI4)gU@Rnz-s$D1inOUm`CLDZ?0na}?_WsXQ z)AzsJlWQm5KR%rFas#01(<&$76m_=AxplE~PHNqeH$nzZC8W@uMazeq z8$>uY=@pN5*2cR8D2+#yOeL)opO_Vlh_jxzbhHi`LcEK2t>yi0kvKoF8Qh3+ZP~oL zT^^3`nlTRPKRK2xf1=*HG!T?GzpMRk0-|O1+`e@|GR7jSUC*}NJD*y~*bE#WOTWXI zfyxYYX4v*@4!LpEQLexRxUa$QoaU~XsC4l_B=hT5Wx1C=;F488yG$+VCKhgzRc{bq zZwUC9+^WV_!BW29l}aJ&2J5ulK}rC^706n36D`T87Z|ApwN;ZP*XEB;AC1ap<}QFk zLB_75YqX`HM+B6fL1`{42}Xw5@QWahQQM~t)Rd_9?@kx-=dJ_}@LD1H`^kg0hJ&e_ z-czB1H}u4(Qrr#j*S)j1qaR0q?kvZhUUzGJ8H@3((yR3Y{96N|u7OGGuBcd?W z4*JeDdu^uQ=^GYlr}H1dipLftzUPd6HzYk(fWJY~`C9?Sw=}lg#~%{D_AGOT2o)X4 za@+_pPS>252nKFyDk4Jl&cM!0!8mN7Y|n-zR}CgMJGL@?R}1wle4o|AY`OUPzvo(7 zyCC}ocM|FaORKj!0_fA}#3v*DnI(#n?K7(i*A6E%jE3 zO|HDo_eO>24e8W=qAi@e%oX_0Gr_U)mRU*L-N7S}#81gov0MXE^Ep;3qwbOee`37$ z*#8kap5E2D=*K%BdU4$UB+Ec3iKw=9D!M~8Vv8j>2F0Cjt?;`&{O3oZU~k@S`T>K( z)i437ek85il@s!;Z%(PTCB`p+pgAQb0)Ax~&-)LW47P66F=CxQq5{??=$f4uaD|^r zy)&4~AoLTPMGqDbs9{rJU47f+l$2q-j(l^0bJieKq&ry>(IaLd$-; zRu0HvrRUfT9}6>^))~)CIZwSlmXxIRrd8^AnM`j)F@SWIpl08q+{X55EuG>bJ>DGu z!6n{b5quV;$}*)4kD~N8WR zyivM`b%gH_dIN0hVn1_Z@Xu+7z?^-=t**mlYIrGX+kN(l!^jBjl3o4y(lB`zi=(Mo zgWaDO3~^J)XU08`oDgjHxAP5=yM3IVCHxqETf~$@qC21S>zi{%%+k7wU+jkMi-zCa zG~#b(7qB^D5W!`-P!ja>gzj_!PDpz;+u&k~QhAiH;#NRX7n@aR(o?qjaQ){d57|=| z(IYTF1SQEQ(w5a#eV^@sF5K0h!;KuKFVB8S8W)+)E{ql?eLFH5&{N#?Ff9sqrN=Pe zVetq}u$}ZlYr7^G=7fzfRlomIe7RkAu`@Wyl{>5?CGo3ENn6>oszg8jdr|lDu+Wr^n)M4%4HP z+Q#OkseK=FhBgN(c4m&LV?tKNgPbJ*^_N% zaJAD6Z8cdX5!{M+V(muf>6eTBh@H*!=H1@SW$~2FU4mA|l0FBKcK3T%khA41WFk39 z#IOWdipLgJpY~RH!*eEPgI`-3DH(=$by@}{=v|C!WB6;AYsDEERt`MpvYgHxPgWM& zQe{>ulIQ~5@8utgo2Cq@b?pmjsXUS zYCuq0_96{06ySMU72H?V>up?2@|KfY5uckq*Du7HFW={j$SoSkLj`??vb0Nzaix`V z!s`92jrP0UxJH`O7Tgp64ejr~uYjP}o8|E|@Pcs7(ldL{5(7_5p*C>IOeH@d=d*kQ z`D5I&x5lx=iMfScCXcGxti0HfP_Mt+OIw7=KD`WiK|3rWy@nAL?m6$ocgk8nGxr_? zRu;io&ox%tH?_lUAX&*MdX6P`t{OF5*Pr-6K+-cOxUB<&%kMqRL(M+X(XN5p@8Q1#gZOt z2MHR2xPckJ%rYjA&3q1<4Y>uwhIb_layT%{f>Li(tWVafCC}j~u(cXuDf3HJ@Ka)l zKEi)HBRu_7F3X{@N+S|XTl>`pO;FMTEtu(fC+}ca-iL#&1?2md;I6e# zf2JEEP6hKutB!pSHO(Wa@h&BwkPsN~;cX6h*&m7hyG73=ch*8TlY_y0&P~nuo|V1~ zr!Wx0`)yvxu^-WWI0k)O5pWHke?w`(2;C*n|15J*E;WDooVBPdM2AMe*hhS!QvN{S z5M?h8*k>;X&z8k1Bf3s+*kY^rqLdKBH&XhNd5&jJlzdSVpn8ebK7uVOUO!)r%msN?CRjyCr1pZ1sb=ChCUpL+B0 zMOPS3=DZH_mHEZoRk~CMlw&S9!tg{D4R z+glWia(>h;`Pw_ZSc}cSMhCBgOB_zNM)h}(jUP^umN5CPf%>6GdIqNqR?#|L-v%bv*;9)J*MLJYvrZzbK3MR#=j24(kl>cq4XMZtwKX#s>B&;VMQSa)FrJ%a7wOEnqO07BYDjHs_9` z2xz+(^e;J7w5}!&geVamxXaJ1!g%E3Xl+1QH*lc-w0892&!?p@=7IaGrlS<@wQo#z zMI#ZqwmkkF@+C|_*gS^@W5T#6q#iuSXMP-UPDWg_e5f6sKs;%w8tIh&`SFWLuK7d= zgHMR8YO@a7M&_51_7?V*c=J#);UP4)4Dgoy+l3Z`ENn1~16ZU}0iNq#BU)NY=Eh>X z>Wc_J-z0VfjIJX)7~b4ynr$!Eci>`Olh+qmE;Qb94owJl+Pm0zeHVFzofOnYKw8eP zm&-qpZD}-DMLlycV+WSauvHXAt+!|{XvS5>J@-GlQ=5g5o>xH7$*9ZJOaf^84#;kc zY@20cR2%(#n0MVBN5X&!#M0wAn+XmpAjKR^)x0hLmjF5cb>mF+CLk?J`vq%MeNAf$ z2PfwVyB2ij^_g*8SVx}oAB?KR@8v1}F-hCc&kW=I(hnLDx_3@a4g~gT;D4WmMahWC zd?s?~+|OzyOjG892gL6IZx-R`YGA_acAf*=n_SO!1#?67JZM815)G87 zg*8|qUZ4FC8BhFJ7U^b&6{JvcA47aV-Vx6qJw+DL*$BtaPANr|#>Y}pY`;V-?vz1N z*@kZvHl~r;yIEWy( z;4Bj-8Q7L{H|wn|&JN+1!FMGbol$Qw7|*~Iz^0$`>w1G2Vg(qUjN~Z{??e^@+wb^cUnqKWK>^Ix z`>vWUchf>p3U6~*@%(O0F)N3^F&pOYHS@$AL7^OjdI#l=z&@utg!R%q zZqNcv|GmZp+P-Z^D73cm7ZaYn68}~A=3|_Xe`0NQMP0YWyLG;*BKKIMLqQ#8+fd*` ziQ5!UHd#c%XRbX`kWp|oH|{+W=cjy!NF@0BNlxmT$J;#KZjf@>wJ}K7RLw!9N{r`Z``Vkp%V#$ArKBkPs;$Pg)`8uNkzS<=$W}HHxh)NRPO4z%6jyeH591ZKmF7{ zlVO$0^7Kt>hp&bxZr9c%MEV^{O7clpQ(fT;PSL^6Hky1M&G&o0-+6zElHW&Zzw-<8 z44naggW)S{RRA>igVcUL z(cV#PN;%wZCx=l0zohm#0=u~ER+Xj+0`-L4lDRiz8j0X7!JiFGnpWh=lXfB0v#en0 z)ksq|xP^2Zc*6hSGmC+`j^u3GMoW9UQONrB_qZwhNQCTbzgc=FbdFJI!qt{Nl@dzL z55yN?IZ7^Ck!S%r=!#?f)Jzm}%7hRY9=FjHn^m4L*(~@r{KukV3mQSQ$-$o;4@G2f z(!kSBWetTJwK*P*BIL{Kk3{K_utPT(Dpi(+8s3R=L&-7PGAjacN8duRy1h$0NK3D7 z-IiWhIKSD@hF4DXYLKUUpCaPbRM%OU5F7hLZv4SuC|2zAfM9~W;p+Vl+VOe@2kgX} z=e1Uqlip#|zM*={;H2^xAm`%CaMUa<7b>-VxAw}()FCtX0N;hmMqK=pcI@4#D0@V8 zrru`X{K`cnV&rkVArn8Uto}9bLqD=_n31@tYKVS>Nx18qd;IomHIta!{|mLe4PmUg;@9MSGj9`M zxcd{V9tL#So20YXvZ=ws^vlO@9wE`N%?dNDLtk4bzZareq%0VOXb{ME-(!BCq)=Eh zn#x5ewFW}e{i77mLE=z=yXf&gAESX}+oX58HA|LZ+6wDOj_7Dr-s1Y5x_S`&zA4JF zFI*SH|CD0i@QG!5?TSIFTVB27*%pSM9IoLR&WJ=56w6&%!FA8?)& zq5Q#og&EFQ6Mb6w*V_};WQ2}2Iv?Z8d5mPHpyvWL)9@(DY;|~oRuze@PjR#OA-sk& z($zf`at!h=DIW@TYRxdR*-?9e;Of}u_!X+Ks+ipJ3^)7b>QeRUq|221xn$)-=Ta-Q zo<(WIP?7qS_Lo?_P4IatOFbis>O20ref&cWqBCtfohNe|$n|ELgf6FxH!jU03ronB z*PnTSdwH}PQCGcL?*=vy_O;Tl?L`aXY|AW`b zhv7W>(Ci)_j5YA?qSz6)YGc;-=9S%kLjtEYpWSX^YIT~JpRLP^nG6P#%RZH<^V+KE z@x#>yY`rMxZs=u4!Jiv9%g{mrtftKs@-s}WM+FdCygowN zAQKj)X3~|XH`I=uHOA$6)6&4_->Ah#Tl!|ZDlCYQ(5Z^CyGr^!UFCk569PZT(!s?# zBGl|?mp?(P9%QE_pe9mwC#JNMP_vGRc^dWDj{!wh=RN{JTJInBm|CmmjHRr6$@IZ+xp)SnTW&>Z77v)7jg7kcK~ zoR27erB7|B_pJlV9;3SqvU}}k5(>5kYaAn{Ce$O|uxf26BJQ@cvlljfY}Ybpia2HU zuTki)!y>6XNhjOJ(&Zl|gxDyJ+@|ahV%yCu?19X?>WY4D67Z7j=V!bSP2p7H!CvgI8(}~r}%!ZO1b+JD>xtg z!XEhAj-gJk<6Hj5+t7rYRY}!sy&tJYLZ`)P1ok!XJ$uLa#t|j!A-)!G|2Hk4e5(+< zToE9n;skEi$+uSfAE|5~nJ%)p(-S63npSll!&!d?2C^74Rh+%8t zdot7(L);n|)ein)nN%j4WzS5J#C%eaZ!pz+v2H+_R^1L86}xXKB=oH4+F+3#xFb>l zqpAp{To;{Nsv+2FPn{X@Mc1&c6`{<#)Pt&>lJIXyFm5GSFp7;Zzh??P+z^zr?*s|DL>O> zaZY=qhgtIQ=%$MZI}Q2Xh0!RHXo=#?!lcw|BSnxyCrW5;-glXVkFH{^_V7niKYJLC zP>nlCZo6{}`S-sz<;$MMd=nr~rZW{UGN|Cc{kG_NT|M+CDO`;2h5}0}X@QM)Y@9FAYtu@Ch{@3-%5f>mPn8el311p{pe2+%m>lWbR;V zWP?R0)>$DjovU%Iq(Y`!&5U`>kS#6@|0ERnd#bsyM>(%ULBYgbSAz4Shr$IkQEf3; z4dJUiFh$N{;xB1(YIlI; zPzD6|Tp6H-Kc41zfcp`S9G|ujRHHZks3in-`K9eXgb5+>N z3;?&q_xQd(-rH*=_h3fdG2iNpjUEiw)2VC?1S?{EVVDa6j!_Q`{~#3{BKceUUNbA) zxb|N%Iow*R!%)-5^*nF?0Tj@3Wh}rwhGG%SR+g8qPGT(5&_HK)UYvAHQNzie*W7Fr zw4v@`PP%v{1f>izoa?GN86omg3pn)LRW%B2gC!sxL^(l2SdW^jZkH2sRN1~`4#mWrw0BfvW)dr<}MKofAtP^BY_B z+X3hE4DGXzBdETOf<$= za_n9q!^O7GAIDNzj)^3P~g+Xt$pG zEyte-(K7NgRP)DuO>P;s1uw&+D#Kn=zaQkcW-$_|#gsIvB%N$InVuBy?hbX8G@=_xGORS88YUXO((t?^p+H zYYI`9*Ajo+M!jNWtZOy9wE{v;mR+uCC?Ke*cm{AGPKe!N6W!p8kO1)Kgj|6H&UzZ# zdjDt=KDpG^K`RIbS2>Bs@?h?mww}L2cQdC-BOIfq;%NlFVqfO*dul(fEJFATkxmZI zblu`Eineo*URUZXT4+#doP?*E(syo*wrl4eFOd}CWY#4f^O|!p3=Gpxr?a;S0FAu9BruS0p=D$-m0Z9Y;m*=Gr0JBkx zn`d|G`9GUvDjRKaUt7jE-sRbiTfRb2w?FCGsS0XA_(kDk+enB7H;QJH-{#VvY$>g-ks z1XTJ7+eeJ%2F)X!)zEKg-=n2;wc;Vi?j+Lc*%(Mh$(=kb_a>Oh)2;n(BcY%=f2vdA8pfEDBzfH4kf3~P8V@a*+RA{XYecd z%FU(s_1UV-e{)*gGN8l3m1(Cpl|um@rx(!{Rs2_t;KnHzy?qkaw^4zEU+}{~z2W2E zD;tB#SchcqkvVZ{R3P;o!l4J<>WeKTfA~jvLPOdmDIx#hR?q01%tp9qxc@Ma50Zxd z;|^T-%gzm=egj)e4UUv(sID<|b!6%rKnHcOZPN(c;wkrbL#uHw3J%9FiZLC=xJ$M! zpDQ9B{5UH*0PGz@1xGXGr6}Fpgb9V(y3;w7>N}yNBmx=E9y;hPZD0n04L%jZ_ zSQlite!T{8nV7|Vw8I6x5$pD0ri~ysah^rN10Yo-vZ;7wG=C6d_Kv8pocc8z?R{zT z<)!5tRTcM|aAtbjnbPXw@i4+YRA6;n$M;KwxA8#PYH-}u7+&tyl~PF;Sf3uucgMf( z*)ti>8x~Q00+vW4@dmMuz$!}AtAOBf%;s6@<)29zd{BZKJy5(a9qcxyXK^T+mdYS# zL~PmY)097UFcaPVNOH`ZiKA!IYWXwL7S>}`QacUqFh_fPekC2uo&TwH?f=vIx_@Pe p|EJQ!|Ftyle^f2LNG4w5o*bB(|Ly-wDmf-oQPfZnzX9t>u&e+8 literal 0 HcmV?d00001 diff --git a/dhis-2/dhis-web-api/pom.xml b/dhis-2/dhis-web-api/pom.xml index af4c8cf15ee4..4bcbe0b9555a 100644 --- a/dhis-2/dhis-web-api/pom.xml +++ b/dhis-2/dhis-web-api/pom.xml @@ -326,6 +326,12 @@ provided + + + org.imgscalr + imgscalr-lib + + org.hamcrest diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/FileResourceController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/FileResourceController.java index 097b50ea1ee6..3cd02b345a46 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/FileResourceController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/FileResourceController.java @@ -30,6 +30,7 @@ import static org.hisp.dhis.dxf2.webmessage.WebMessageUtils.error; import static org.hisp.dhis.dxf2.webmessage.WebMessageUtils.notFound; import static org.hisp.dhis.dxf2.webmessage.WebMessageUtils.unauthorized; +import static org.hisp.dhis.webapi.utils.FileResourceUtils.resizeAvatarToDefaultSize; import com.google.common.base.MoreObjects; import java.io.IOException; @@ -119,10 +120,12 @@ public void getFileResourceData( } response.setContentType(fileResource.getContentType()); + response.setHeader( HttpHeaders.CONTENT_LENGTH, String.valueOf(fileResourceService.getFileResourceContentLength(fileResource))); response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "filename=" + fileResource.getName()); + HeaderUtils.setSecurityHeaders( response, dhisConfig.getProperty(ConfigurationKey.CSP_HEADER_VALUE)); @@ -143,8 +146,23 @@ public WebMessage saveFileResource( @RequestParam MultipartFile file, @RequestParam(defaultValue = "DATA_VALUE") FileResourceDomain domain, @RequestParam(required = false) String uid) - throws WebMessageException, IOException { - FileResource fileResource = fileResourceUtils.saveFileResource(uid, file, domain); + throws IOException, WebMessageException { + FileResource fileResource; + + if (domain.equals(FileResourceDomain.USER_AVATAR)) { + fileResourceUtils.validateUserAvatar(file); + fileResource = + fileResourceUtils.saveFileResource(uid, resizeAvatarToDefaultSize(file), domain); + + } else if (domain.equals(FileResourceDomain.ORG_UNIT)) { + fileResourceUtils.validateOrgUnitImage(file); + fileResource = + fileResourceUtils.saveFileResource( + uid, fileResourceUtils.resizeOrgToDefaultSize(file), domain); + + } else { + fileResource = fileResourceUtils.saveFileResource(uid, file, domain); + } WebMessage webMessage = new WebMessage(Status.OK, HttpStatus.ACCEPTED); webMessage.setResponse(new FileResourceWebMessageResponse(fileResource)); diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/FileResourceUtils.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/FileResourceUtils.java index f1ff228a7dbc..30a700723dfc 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/FileResourceUtils.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/FileResourceUtils.java @@ -27,21 +27,32 @@ */ package org.hisp.dhis.webapi.utils; +import static org.apache.commons.io.FilenameUtils.getExtension; import static org.hisp.dhis.dxf2.webmessage.WebMessageUtils.conflict; import static org.hisp.dhis.dxf2.webmessage.WebMessageUtils.error; import static org.hisp.dhis.external.conf.ConfigurationKey.CSP_HEADER_VALUE; +import static org.imgscalr.Scalr.resize; import com.google.common.hash.Hashing; import com.google.common.io.ByteSource; +import java.awt.image.BufferedImage; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Files; +import java.util.List; +import java.util.Objects; +import javax.imageio.ImageIO; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.input.NullInputStream; import org.apache.commons.lang3.StringUtils; +import org.hisp.dhis.common.IllegalQueryException; import org.hisp.dhis.dxf2.webmessage.WebMessageException; import org.hisp.dhis.external.conf.DhisConfigurationProvider; import org.hisp.dhis.feedback.ErrorCode; @@ -49,12 +60,14 @@ import org.hisp.dhis.fileresource.FileResourceDomain; import org.hisp.dhis.fileresource.FileResourceService; import org.hisp.dhis.fileresource.ImageFileDimension; +import org.imgscalr.Scalr.Mode; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; import org.springframework.util.InvalidMimeTypeException; import org.springframework.util.MimeTypeUtils; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.commons.CommonsMultipartFile; /** * @author Lars Helge Overland @@ -62,8 +75,47 @@ @Component @Slf4j public class FileResourceUtils { + @Autowired private FileResourceService fileResourceService; + private static final List CUSTOM_ICON_VALID_ICON_EXTENSIONS = List.of("png"); + + private static final long CUSTOM_ICON_FILE_SIZE_LIMIT_IN_BYTES = 25_000_000; + + private static final int CUSTOM_ICON_TARGET_HEIGHT = 48; + private static final int CUSTOM_ICON_TARGET_WIDTH = 48; + + private static final int AVATAR_TARGET_HEIGHT = 200; + private static final int AVATAR_TARGET_WIDTH = 200; + + private static final int ORGUNIT_TARGET_HEIGHT = 800; + private static final int ORGUNIT_TARGET_WIDTH = 800; + + private static final long MAX_AVATAR_IMAGE_SIZE_IN_BYTES = 2_000_000; + private static final long MAX_ORGUNIT_IMAGE_SIZE_IN_BYTES = 8_000_000; + + private static final List ALLOWED_IMAGE_FILE_EXTENSIONS = + List.of("jpg", "jpeg", "png", "gif"); + private static final List ALLOWED_IMAGE_MIME_TYPES = + List.of("image/jpeg", "image/png", "image/gif"); + + private static class MultipartFileByteSource extends ByteSource { + private MultipartFile file; + + public MultipartFileByteSource(MultipartFile file) { + this.file = file; + } + + @Override + public InputStream openStream() { + try { + return file.getInputStream(); + } catch (IOException ioe) { + return new NullInputStream(0); + } + } + } + /** * Transfers the given multipart file content to a local temporary file. * @@ -197,20 +249,121 @@ public FileResource saveFileResource(String uid, MultipartFile file, FileResourc // Inner classes // ------------------------------------------------------------------------- - private class MultipartFileByteSource extends ByteSource { - private MultipartFile file; + // private class MultipartFileByteSource extends ByteSource { + // private MultipartFile file; + // + // public MultipartFileByteSource(MultipartFile file) { + // this.file = file; + // } + // + // @Override + // public InputStream openStream() throws IOException { + // try { + // return file.getInputStream(); + // } catch (IOException ioe) { + // return new NullInputStream(0); + // } + // } + // } - public MultipartFileByteSource(MultipartFile file) { - this.file = file; + public void validateOrgUnitImage(MultipartFile file) { + validateContentType(file.getContentType(), ALLOWED_IMAGE_MIME_TYPES); + validateFileExtension(file.getOriginalFilename(), ALLOWED_IMAGE_FILE_EXTENSIONS); + validateFileSize(file, MAX_ORGUNIT_IMAGE_SIZE_IN_BYTES); + } + + public void validateUserAvatar(MultipartFile file) { + validateContentType(file.getContentType(), ALLOWED_IMAGE_MIME_TYPES); + validateFileExtension(file.getOriginalFilename(), ALLOWED_IMAGE_FILE_EXTENSIONS); + validateFileSize(file, MAX_AVATAR_IMAGE_SIZE_IN_BYTES); + } + + private void validateContentType(String contentType, List validExtensions) { + if (contentType == null) { + throw new IllegalQueryException("Invalid content type, content type is NULL"); + } + contentType = contentType.split(";")[0].trim(); + if (!validExtensions.contains(contentType)) { + throw new IllegalQueryException( + "Invalid content type, valid content types are: " + String.join(",", validExtensions)); } + } - @Override - public InputStream openStream() throws IOException { - try { - return file.getInputStream(); - } catch (IOException ioe) { - return new NullInputStream(0); + public static void validateCustomIconFile(MultipartFile file) { + validateFileExtension(file.getOriginalFilename(), CUSTOM_ICON_VALID_ICON_EXTENSIONS); + validateFileSize(file, CUSTOM_ICON_FILE_SIZE_LIMIT_IN_BYTES); + } + + private static void validateFileExtension(String fileName, List validExtension) { + if (getExtension(fileName) == null || !validExtension.contains(getExtension(fileName))) { + throw new IllegalQueryException( + "Wrong file extension, valid extensions are: " + String.join(",", validExtension)); + } + } + + private static void validateFileSize(MultipartFile file, long maxFileSizeInBytes) { + if (file.getSize() > maxFileSizeInBytes) { + throw new IllegalQueryException( + String.format( + "File size can't be bigger than %d, current file size %d", + maxFileSizeInBytes, file.getSize())); + } + } + + public static MultipartFile resizeImageToCustomSize( + MultipartFile multipartFile, int targetWidth, int targetHeight, Mode resizeMode) + throws IOException { + File tmpFile = null; + + try { + BufferedImage resizedImage = + resize( + ImageIO.read(multipartFile.getInputStream()), resizeMode, targetWidth, targetHeight); + + tmpFile = Files.createTempFile("org.hisp.dhis", ".tmp").toFile(); + + ImageIO.write( + resizedImage, + Objects.requireNonNull(getExtension(multipartFile.getOriginalFilename())), + tmpFile); + + FileItem fileItem = + new DiskFileItemFactory() + .createItem( + "file", + Files.probeContentType(tmpFile.toPath()), + false, + multipartFile.getOriginalFilename()); + + try (InputStream in = new FileInputStream(tmpFile); + OutputStream out = fileItem.getOutputStream()) { + in.transferTo(out); + } + + return new CommonsMultipartFile(fileItem); + } catch (Exception e) { + throw new IOException("Failed to resize image: " + e.getMessage()); + } finally { + if (tmpFile != null && tmpFile.exists()) { + Files.delete(tmpFile.toPath()); } } } + + public static MultipartFile resizeIconToDefaultSize(MultipartFile multipartFile) + throws IOException { + return resizeImageToCustomSize( + multipartFile, CUSTOM_ICON_TARGET_WIDTH, CUSTOM_ICON_TARGET_HEIGHT, Mode.FIT_EXACT); + } + + public static MultipartFile resizeAvatarToDefaultSize(MultipartFile multipartFile) + throws IOException { + return resizeImageToCustomSize( + multipartFile, AVATAR_TARGET_WIDTH, AVATAR_TARGET_HEIGHT, Mode.AUTOMATIC); + } + + public MultipartFile resizeOrgToDefaultSize(MultipartFile multipartFile) throws IOException { + return resizeImageToCustomSize( + multipartFile, ORGUNIT_TARGET_WIDTH, ORGUNIT_TARGET_HEIGHT, Mode.AUTOMATIC); + } }