11package com .k4m .dx .tcontrol .db2pg .history .web ;
22
33import java .io .File ;
4+ import java .io .FileInputStream ;
5+ import java .io .IOException ;
46import java .text .SimpleDateFormat ;
57import java .util .ArrayList ;
68import java .util .HashMap ;
79import java .util .List ;
810import java .util .Map ;
11+ import java .util .Properties ;
912
1013import javax .servlet .http .HttpServletRequest ;
1114import javax .servlet .http .HttpServletResponse ;
1619import org .springframework .stereotype .Controller ;
1720import org .springframework .web .bind .annotation .ModelAttribute ;
1821import org .springframework .web .bind .annotation .RequestMapping ;
22+ import org .springframework .util .ResourceUtils ;
1923import org .springframework .web .bind .annotation .ResponseBody ;
2024import org .springframework .web .servlet .ModelAndView ;
2125
2226import com .k4m .dx .tcontrol .admin .accesshistory .service .AccessHistoryService ;
27+ import com .k4m .dx .tcontrol .admin .menuauthority .service .MenuAuthorityService ;
2328import com .k4m .dx .tcontrol .cmmn .CmmnUtils ;
2429import com .k4m .dx .tcontrol .common .service .HistoryVO ;
2530import com .k4m .dx .tcontrol .db2pg .cmmn .DB2PG_LOG ;
@@ -37,7 +42,10 @@ public class Db2pgHistoryController {
3742
3843 @ Autowired
3944 private AccessHistoryService accessHistoryService ;
40-
45+
46+ @ Autowired
47+ private MenuAuthorityService menuAuthorityService ;
48+
4149 /**
4250 * DB2PG 수행이력 화면을 보여준다.
4351 *
@@ -228,7 +236,10 @@ public ModelAndView db2pgResult(@ModelAttribute("historyVO") HistoryVO historyVO
228236
229237 try {
230238 String trans_save_pth = request .getParameter ("trans_save_pth" );
231- db2pgResult = DB2PG_LOG .db2pgFile (trans_save_pth );
239+ // 보안: db2pg 루트 하위 경로만 허용(경로 조작 방지)
240+ if (resolveUnderDb2pgRoot (trans_save_pth ) != null ) {
241+ db2pgResult = DB2PG_LOG .db2pgFile (trans_save_pth );
242+ }
232243 } catch (Exception e ) {
233244 e .printStackTrace ();
234245 }
@@ -260,12 +271,16 @@ public ModelAndView db2pgProgress( HttpServletRequest request) {
260271 List <String > lines = null ;
261272 String [] result = null ;
262273 try {
263- String trans_save_pth = request .getParameter ("trans_save_pth" );
264- lines = DB2PG_LOG .readLastLine (new File (trans_save_pth +"/result/progress.txt" ), 1 );
274+ String trans_save_pth = request .getParameter ("trans_save_pth" );
275+ // 보안: db2pg 루트 하위 경로만 허용(경로 조작 방지)
276+ File progressDir = resolveUnderDb2pgRoot (trans_save_pth );
277+ if (progressDir != null ) {
278+ lines = DB2PG_LOG .readLastLine (new File (progressDir , "result/progress.txt" ), 1 );
279+ }
265280 } catch (Exception e ) {
266281 System .out .println ("* cannot found progress.txt" );
267282 }
268- if (lines .size () > 0 && lines .get (0 ).contains ("," )){
283+ if (lines != null && lines .size () > 0 && lines .get (0 ).contains ("," )){
269284 result = lines .get (0 ).split ("," );
270285 mv .addObject ("totalcnt" ,result [0 ]);
271286 mv .addObject ("nowcnt" ,result [1 ]);
@@ -332,8 +347,9 @@ public ModelAndView db2pgResultDDL(@ModelAttribute("historyVO") HistoryVO histor
332347 String pattern = "yyyy-MM-dd HH:mm:ss" ;
333348 SimpleDateFormat simpleDateFormat = new SimpleDateFormat (pattern );
334349
335- File dirFile = new File (ddl_save_pth );
336- File [] fileList = dirFile .listFiles ();
350+ // 보안: db2pg 루트 하위 디렉토리만 허용(경로 조작 방지)
351+ File dirFile = resolveUnderDb2pgRoot (ddl_save_pth );
352+ File [] fileList = (dirFile != null ) ? dirFile .listFiles () : null ;
337353
338354 if (fileList !=null ){
339355 for (int i =0 ; i < fileList .length ; i ++){
@@ -352,30 +368,89 @@ public ModelAndView db2pgResultDDL(@ModelAttribute("historyVO") HistoryVO histor
352368 return result ;
353369 }
354370
371+ /**
372+ * 보안: 사용자 입력 경로가 db2pg 루트(db2pg_path) 하위인지 검증한다.
373+ * 유효하면 정규화된 File을, 루트 이탈·오류·빈값이면 null을 반환한다(경로 조작 방지).
374+ */
375+ private File resolveUnderDb2pgRoot (String userPath ) {
376+ if (userPath == null || userPath .trim ().isEmpty ()) {
377+ return null ;
378+ }
379+ try {
380+ Properties props = new Properties ();
381+ try (FileInputStream in = new FileInputStream (
382+ ResourceUtils .getFile ("classpath:egovframework/tcontrolProps/globals.properties" ))) {
383+ props .load (in );
384+ }
385+ File root = new File (props .getProperty ("db2pg_path" )).getCanonicalFile ();
386+ File target = new File (userPath ).getCanonicalFile ();
387+ if (target .getPath ().equals (root .getPath ())
388+ || target .getPath ().startsWith (root .getPath () + File .separator )) {
389+ return target ;
390+ }
391+ } catch (Exception e ) {
392+ // 검증 실패는 거부(null)로 처리
393+ }
394+ return null ;
395+ }
396+
355397 /**
356398 * DDL 수행이력 결과를 파일로 다운받는다.
357- *
399+ *
358400 * @param request
359401 * @param response
360402 */
361403 @ RequestMapping (value = "/db2pg/popup/db2pgFileDownload.do" )
362404 public void fileDownload (HttpServletRequest request , HttpServletResponse response ){
363405 try {
364- //파일경로입력
365-
366- System .out .println ("파일경로=" +request .getParameter ("path" ));
367- System .out .println ("파일명=" +request .getParameter ("name" ));
368-
369- String filePath = request .getParameter ("path" );
370- String fileName = request .getParameter ("name" );
371- String viewFileNm = request .getParameter ("name" );
406+ // 인가: DB2PG 수행이력(MN00017) 읽기 권한 확인
407+ CmmnUtils cu = new CmmnUtils ();
408+ List <Map <String , Object >> menuAut = cu .selectMenuAut (menuAuthorityService , "MN00017" );
409+ if (menuAut == null || menuAut .isEmpty () || "N" .equals (menuAut .get (0 ).get ("read_aut_yn" ))) {
410+ response .sendError (HttpServletResponse .SC_FORBIDDEN );
411+ return ;
412+ }
413+
414+ String reqPath = request .getParameter ("path" );
415+ String reqName = request .getParameter ("name" );
416+ if (reqPath == null || reqPath .trim ().isEmpty () || reqName == null || reqName .trim ().isEmpty ()) {
417+ response .sendError (HttpServletResponse .SC_BAD_REQUEST );
418+ return ;
419+ }
420+
421+ // 보안: 파일명은 basename만 사용하여 경로 구분자·상위경로(../)를 제거한다.
422+ String safeName = new File (reqName ).getName ();
423+
424+ // 보안: 다운로드 루트(db2pg_path) 하위로만 허용한다(canonical 경로 prefix 검증).
425+ Properties props = new Properties ();
426+ try (FileInputStream in = new FileInputStream (
427+ ResourceUtils .getFile ("classpath:egovframework/tcontrolProps/globals.properties" ))) {
428+ props .load (in );
429+ }
430+ File root = new File (props .getProperty ("db2pg_path" )).getCanonicalFile ();
431+ File target = new File (reqPath , safeName ).getCanonicalFile ();
432+ if (!target .getPath ().equals (root .getPath ())
433+ && !target .getPath ().startsWith (root .getPath () + File .separator )) {
434+ response .sendError (HttpServletResponse .SC_FORBIDDEN );
435+ return ;
436+ }
437+ if (!target .exists () || !target .isFile ()) {
438+ response .sendError (HttpServletResponse .SC_NOT_FOUND );
439+ return ;
440+ }
441+
442+ // 검증된 경로/파일명으로만 다운로드한다.
372443 DownloadView fileDown = new DownloadView (); //파일다운로드 객체생성
373- fileDown .filDown (request , response , filePath , fileName , viewFileNm ); //파일다운로드
444+ fileDown .filDown (request , response , target . getParent () + File . separator , target . getName (), safeName ); //파일다운로드
374445
375446 } catch (Exception e ) {
376447 e .printStackTrace ();
448+ try {
449+ response .sendError (HttpServletResponse .SC_INTERNAL_SERVER_ERROR );
450+ } catch (IOException ignore ) {
451+ }
377452 }
378- }
453+ }
379454
380455 @ RequestMapping (value = "/db2pg/cancel.do" )
381456 public @ ResponseBody JSONObject db2pgCancel (HttpServletRequest request ){
0 commit comments