UI5

SAPUI5 보안 완전 가이드 — XSS 방지, CSRF 토큰, CSP 설정부터 XSUAA 연동까지

▶ YouTube에서 보기

SAPUI5 보안 완전 가이드 — XSS 방지, CSRF 토큰, CSP 설정부터 XSUAA 연동까지

Moderator · 2026. 4. 28. · 조회 3

SAPUI5 보안 완전 가이드 — XSS 방지, CSRF 토큰, CSP 설정부터 XSUAA 연동까지

Moderator · 2026. 4. 28. · 조회 3
SAPUI5 보안 완전 가이드 SAPUI5 보안 핵심 요소 SAPUI5 보안 가이드 CTA

SAPUI5 보안 완전 가이드 — XSS 방지, CSRF 토큰, CSP 설정부터 XSUAA 연동까지

엔터프라이즈 SAPUI5 애플리케이션에서 반드시 점검해야 할 보안 영역을 체계적으로 다룹니다. 클라이언트 측 XSS 방어부터 서버 측 CSRF 토큰 처리, Content Security Policy 적용, 그리고 SAP BTP 환경의 XSUAA 인증/인가 연동까지 실무 코드와 함께 살펴봅니다.

TL;DR — 5분 안에 알아둘 것

SAPUI5로 구축한 Fiori 앱은 브라우저에서 실행되는 싱글 페이지 애플리케이션(SPA)입니다. SPA 특성상 클라이언트 측에서 HTML을 동적으로 조작하는 빈도가 높아, XSS(Cross-Site Scripting)와 같은 인젝션 공격에 노출되기 쉽습니다. 또한 OData 서비스 호출 시 CSRF(Cross-Site Request Forgery) 토큰을 올바르게 처리하지 않으면 위변조 요청이 통과될 수 있고, CSP(Content Security Policy) 미설정 시 외부 스크립트 삽입 위험이 존재합니다.

이 글을 완료하면 다음을 수행할 수 있습니다.

  • SAPUI5 컨트롤의 기본 XSS 방어 메커니즘을 이해하고, 위험한 패턴을 식별할 수 있다
  • OData 및 fetch API에서 CSRF 토큰을 올바르게 처리할 수 있다
  • CSP 헤더와 meta 태그를 설정하여 스크립트 인젝션을 차단할 수 있다
  • SAP BTP 환경에서 XSUAA를 활용한 OAuth2 인증/인가 흐름을 구성할 수 있다
  • 보안 체크리스트를 적용하여 프로덕션 배포 전 취약점을 점검할 수 있다

사전 가정

이 가이드를 효과적으로 따라가려면 아래 항목에 대한 기본적인 이해가 필요합니다.

  • SAPUI5 MVC 패턴과 XML View 작성 경험
  • OData V2/V4 서비스 바인딩 기본 개념
  • HTTP 프로토콜과 헤더(Cookie, Authorization) 기초
  • SAP BTP(Business Technology Platform) 계정 및 서브어카운트 구조 이해
  • 웹 보안 기초 — OWASP Top 10 항목에 대한 개괄적 인식

테스트 환경

항목권장 사양
SAPUI5 버전1.120 LTS 이상 (2024 Q4 기준 최신 LTS)
SAP BTP 에디션Free Tier 또는 Enterprise (Trial 가능)
개발 도구SAP Business Application Studio 또는 VS Code + Fiori Tools
런타임Cloud Foundry 또는 Kyma (Node.js 18+)
서비스XSUAA 서비스 인스턴스, Destination 서비스

로컬 개발 시 ui5 serve 명령어로 개발 서버를 실행하며, --open 플래그와 함께 HTTPS를 활성화하는 것이 보안 테스트에 유리합니다. CSP 헤더 테스트를 위해 Chrome DevTools의 Network 탭과 Console 탭을 함께 활용합니다.

4. 핵심 개념

XSS 방어 — 자동 인코딩이라는 방패

SAPUI5의 표준 컨트롤(예: sap.m.Text, sap.m.Label)은 내부적으로 렌더러가 텍스트를 DOM에 삽입할 때 HTML 엔티티 인코딩을 자동 수행합니다. 이를 비유하면, 모든 택배 상자(사용자 입력)가 배송 센터(렌더러)를 거치면서 자동으로 X-ray 검사를 받는 것과 같습니다. 그러나 sap.ui.core.HTML 컨트롤이나 jQuery.html()처럼 원시 HTML을 직접 삽입하는 경로는 이 검사를 우회하므로, 별도의 수동 검증이 필수입니다.

CSRF 토큰 — 요청의 신원 확인

CSRF 공격은 사용자가 인증된 세션을 가진 상태에서 악의적 페이지가 해당 세션을 도용하여 요청을 보내는 방식입니다. SAPUI5의 OData 모델(sap.ui.model.odata.v2.ODataModel)은 첫 번째 변경(mutation) 요청 전에 자동으로 X-CSRF-Token: Fetch 헤더를 보내 토큰을 획득하고, 이후 POST/PUT/DELETE 요청에 해당 토큰을 자동 첨부합니다. 이를 "은행 창구의 번호표"에 비유할 수 있습니다. 먼저 번호표(토큰)를 받고, 그 번호표를 제시해야만 거래(데이터 변경)가 가능합니다.

CSP — 허용 목록 기반 방화벽

Content Security Policy는 브라우저에게 "이 출처의 스크립트/스타일/이미지만 허용하라"고 지시하는 HTTP 응답 헤더입니다. SAPUI5 앱에서는 UI5 CDN, 자체 도메인, 그리고 OData 서비스 도메인만 허용하는 정책을 설정하는 것이 일반적입니다. unsafe-inlineunsafe-eval을 가능한 한 배제하는 것이 권장되지만, SAPUI5 일부 레거시 기능이 eval을 사용하므로 점진적 적용이 현실적입니다.

XSUAA — BTP 환경의 중앙 인증 관리

SAP Authorization and Trust Management Service(XSUAA)는 OAuth 2.0 기반으로 토큰을 발급하고, 앱 라우터(approuter)가 이를 중개합니다. 사용자 브라우저 → approuter → XSUAA → JWT 토큰 발급 → 백엔드 서비스 검증의 흐름을 따릅니다.

직접 해보기 (3단계)

1단계: 기본 예제 — XSS 안전한 렌더링 vs 위험한 패턴

아래 코드는 사용자 입력을 화면에 표시할 때 안전한 방법과 위험한 방법을 비교합니다.

<!-- XML View: 안전한 패턴 -->
<mvc:View xmlns:mvc="sap.ui.core.mvc" xmlns="sap.m">
  <!-- sap.m.Text는 자동으로 HTML 인코딩 수행 -->
  <Text text="{/userComment}" />

  <!-- sap.m.FormattedText는 허용된 태그만 렌더링 -->
  <FormattedText htmlText="{/safeHtml}" />
</mvc:View>
// Controller: 위험한 패턴 (절대 사용 금지)
onAfterRendering: function () {
  // 위험: 사용자 입력을 innerHTML로 직접 삽입
  var sUserInput = this.getModel().getProperty("/userComment");
  document.getElementById("output").innerHTML = sUserInput;
  // 만약 sUserInput이 "<script>alert('XSS')</script>"라면 스크립트 실행됨
}

// 안전한 패턴: jQuery의 text() 또는 SAPUI5 컨트롤 사용
onAfterRendering: function () {
  var sUserInput = this.getModel().getProperty("/userComment");
  // jQuery.text()는 HTML 태그를 이스케이프 처리
  jQuery("#output").text(sUserInput);
}

// 더 안전한 패턴: SAP 제공 인코딩 유틸리티 사용
sap.ui.require(["sap/base/security/encodeXML"], function (encodeXML) {
  var sSafe = encodeXML(sUserInput);
  // sSafe: "&lt;script&gt;alert('XSS')&lt;/script&gt;"
});

sap/base/security 모듈에는 encodeXML, encodeJS, encodeURL, encodeCSS 등 컨텍스트별 인코딩 함수가 제공됩니다. 출력 위치(HTML 속성, JavaScript 문자열, URL 파라미터 등)에 맞는 인코딩 함수를 선택해야 합니다.

2단계: 실무 시나리오 — CSRF 토큰 수동 처리 (fetch API)

OData 모델을 사용하지 않고 직접 REST API를 호출하는 경우, CSRF 토큰을 수동으로 관리해야 합니다. 아래는 fetch API를 활용한 2단계 토큰 처리 패턴입니다.

sap.ui.define([
  "sap/ui/core/mvc/Controller",
  "sap/m/MessageToast",
  "sap/base/Log"
], function (Controller, MessageToast, Log) {
  "use strict";

  return Controller.extend("myapp.controller.Main", {

    _sCsrfToken: null,

    /**
     * 1단계: CSRF 토큰 Fetch
     * HEAD 또는 GET 요청으로 토큰을 획득한다
     */
    _fetchCsrfToken: async function () {
      try {
        var oResponse = await fetch("/api/service/", {
          method: "HEAD",
          headers: {
            "X-CSRF-Token": "Fetch"
          },
          credentials: "include" // 쿠키 포함 필수
        });

        if (!oResponse.ok) {
          throw new Error("CSRF 토큰 요청 실패: " + oResponse.status);
        }

        this._sCsrfToken = oResponse.headers.get("X-CSRF-Token");
        Log.info("CSRF 토큰 획득 성공");
        return this._sCsrfToken;

      } catch (oError) {
        Log.error("CSRF 토큰 획득 오류", oError.message);
        throw oError;
      }
    },

    /**
     * 2단계: 토큰을 포함한 데이터 변경 요청
     */
    onSavePress: async function () {
      try {
        // 토큰이 없거나 만료되었을 경우 재획득
        if (!this._sCsrfToken) {
          await this._fetchCsrfToken();
        }

        var oPayload = this.getView().getModel().getProperty("/editData");

        var oResponse = await fetch("/api/service/Orders", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "X-CSRF-Token": this._sCsrfToken
          },
          credentials: "include",
          body: JSON.stringify(oPayload)
        });

        // 403: 토큰 만료 → 재시도 1회
        if (oResponse.status === 403) {
          Log.warning("CSRF 토큰 만료, 재획득 시도");
          this._sCsrfToken = null;
          await this._fetchCsrfToken();

          oResponse = await fetch("/api/service/Orders", {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              "X-CSRF-Token": this._sCsrfToken
            },
            credentials: "include",
            body: JSON.stringify(oPayload)
          });
        }

        if (!oResponse.ok) {
          throw new Error("저장 실패: " + oResponse.status);
        }

        MessageToast.show("저장 완료");

      } catch (oError) {
        Log.error("저장 오류", oError.message);
        MessageToast.show("오류가 발생했습니다: " + oError.message);
      }
    }
  });
});

주요 포인트: credentials: "include"를 반드시 설정해야 세션 쿠키가 전송되어 서버가 토큰과 세션을 매칭할 수 있습니다. 토큰 만료 시 403 응답이 오는 것이 일반적이므로, 1회 자동 재시도 로직을 포함하는 것이 실무에서 권장됩니다.

3단계: 프로덕션 — CSP 설정 및 XSUAA 연동

프로덕션 배포 시 CSP 헤더는 앱 라우터(approuter) 또는 웹 서버 레벨에서 설정합니다.

// xs-app.json (SAP approuter 설정)
{
  "welcomeFile": "/index.html",
  "authenticationMethod": "route",
  "routes": [
    {
      "source": "^/api/(.*)$",
      "target": "$1",
      "destination": "backend-api",
      "authenticationType": "xsuaa",
      "csrfProtection": true
    },
    {
      "source": "^(.*)$",
      "target": "$1",
      "service": "html5-apps-repo-rt",
      "authenticationType": "xsuaa"
    }
  ],
  "responseHeaders": [
    {
      "name": "Content-Security-Policy",
      "value": "default-src 'self'; script-src 'self' https://ui5.sap.com; style-src 'self' 'unsafe-inline' https://ui5.sap.com; font-src 'self' https://ui5.sap.com data:; img-src 'self' https://ui5.sap.com data: blob:; connect-src 'self' https://*.hana.ondemand.com; frame-ancestors 'self' https://*.hana.ondemand.com;"
    },
    {
      "name": "X-Content-Type-Options",
      "value": "nosniff"
    },
    {
      "name": "X-Frame-Options",
      "value": "SAMEORIGIN"
    }
  ]
}
// xs-security.json (XSUAA 서비스 설정)
{
  "xsappname": "myapp-security-demo",
  "tenant-mode": "dedicated",
  "scopes": [
    {
      "name": "$XSAPPNAME.Display",
      "description": "조회 권한"
    },
    {
      "name": "$XSAPPNAME.Admin",
      "description": "관리자 권한"
    }
  ],
  "role-templates": [
    {
      "name": "Viewer",
      "description": "읽기 전용 사용자",
      "scope-references": ["$XSAPPNAME.Display"]
    },
    {
      "name": "Administrator",
      "description": "전체 관리 권한",
      "scope-references": ["$XSAPPNAME.Display", "$XSAPPNAME.Admin"]
    }
  ],
  "role-collections": [
    {
      "name": "MyApp_Viewer",
      "role-template-references": ["$XSAPPNAME.Viewer"]
    },
    {
      "name": "MyApp_Admin",
      "role-template-references": ["$XSAPPNAME.Administrator"]
    }
  ]
}
// Node.js 백엔드에서 JWT 토큰 검증 (CAP 또는 Express 미들웨어)
const xsenv = require("@sap/xsenv");
const passport = require("passport");
const { JWTStrategy } = require("@sap/xssec");

// XSUAA 서비스 바인딩 정보 로드
const xsuaaCredentials = xsenv.getServices({ xsuaa: { tag: "xsuaa" } });

// Passport JWT 전략 등록
passport.use("JWT", new JWTStrategy(xsuaaCredentials.xsuaa));

app.use(passport.initialize());
app.use(passport.authenticate("JWT", { session: false }));

// 스코프 기반 인가 체크
app.get("/api/admin/settings", function (req, res) {
  var sScope = xsuaaCredentials.xsuaa.xsappname + ".Admin";
  if (!req.authInfo.checkScope(sScope)) {
    res.status(403).json({ error: "관리자 권한이 필요합니다" });
    return;
  }
  res.json({ settings: "..." });
});

CSP 설정에서 script-src'unsafe-eval'을 넣지 않는 것이 이상적이나, SAPUI5 1.120 이전 버전에서는 일부 바인딩 표현식이 eval을 사용합니다. SAPUI5 팀에서는 이를 점진적으로 제거하고 있으며, 최신 버전에서는 xx-bindingSyntax: "complex" 설정으로 eval 의존도를 낮출 수 있습니다.

삽질 노트 — 자주 만나는 함정

FAQ 1: CSP 위반으로 UI5 앱이 아예 로딩되지 않습니다

원인: script-src에 UI5 CDN 도메인을 포함하지 않았거나, 'unsafe-inline' 없이 인라인 이벤트 핸들러가 존재하는 경우입니다. Chrome DevTools Console에서 Refused to execute inline script 메시지를 확인하세요.

해결: 개발 단계에서는 Content-Security-Policy-Report-Only 헤더로 위반 사항만 기록하고, 점진적으로 정책을 강화합니다. UI5 CDN 사용 시 https://ui5.sap.comscript-srcstyle-src에 반드시 추가합니다.

FAQ 2: CSRF 토큰이 계속 403을 반환합니다

원인: 가장 흔한 원인은 credentials 옵션 누락입니다. fetch()는 기본적으로 쿠키를 전송하지 않으므로 서버가 세션을 인식하지 못합니다. 또한 CORS 환경에서는 서버의 Access-Control-Allow-Credentials: true 설정도 필요합니다.

해결: credentials: "include"를 fetch 옵션에 추가하고, 서버 CORS 설정에서 allowCredentials를 활성화합니다. approuter를 경유하면 동일 출처이므로 CORS 이슈를 우회할 수 있습니다.

FAQ 3: XSUAA 토큰에 스코프가 비어 있습니다

원인: Role Collection을 생성했지만 BTP Cockpit에서 사용자에게 할당하지 않은 경우입니다. xs-security.json의 스코프 이름과 코드에서 체크하는 스코프 이름이 일치하지 않는 오타도 빈번합니다.

해결: BTP Cockpit > Security > Users에서 해당 사용자에게 Role Collection을 할당합니다. JWT 토큰을 jwt.io에서 디코딩하여 scope 배열을 직접 확인하는 것이 디버깅에 효과적입니다.

FAQ 4: sap.ui.core.HTML 컨트롤에서 스크립트가 실행됩니다

원인: sap.ui.core.HTML은 기본적으로 sanitizeContent 속성이 false입니다. 사용자 입력이 이 컨트롤에 바인딩되면 스크립트 인젝션이 가능합니다.

해결: sanitizeContent="true"를 설정하거나, 가능하면 sap.m.FormattedText로 대체합니다. FormattedText는 허용된 HTML 태그(b, i, em, strong, a, p, br 등)만 렌더링하고 나머지는 제거합니다.

더 파볼 주제

이 가이드에서 다룬 보안 기초를 확보한 후, 아래 주제로 확장하는 것을 권장합니다.

  • SAP Cloud Application Programming Model(CAP) 보안 — CDS 레벨의 @requires, @restrict 어노테이션을 활용한 선언적 인가 관리
  • SAP Build Work Zone 통합 보안 — 서비스 간 SSO(Single Sign-On) 설정과 Launchpad 보안 컨텍스트 전파
  • SAPUI5 애플리케이션 감사(Audit) 로깅 — SAP Audit Log Service와의 연동으로 사용자 행위 추적
  • 자동화된 보안 테스트 — OWASP ZAP 프록시를 활용한 SAPUI5 앱 자동 취약점 스캔
  • SAP Cloud Connector 보안 — On-Premise 시스템 연결 시 TLS 터널링 및 접근 제어 설정

더 읽어볼 자료


⚠️ 비공식 콘텐츠 안내

본 게시글은 btpstacks.com의 독립 학습 콘텐츠이며 SAP SE와 무관합니다. 공식 문서는 help.sap.com을 참고하세요.

SAP, ABAP, SAP BTP, SAPUI5, SAP Fiori는 독일 및 기타 국가에서 SAP SE의 상표 또는 등록상표입니다.

댓글 0

아직 댓글이 없습니다.