SAPUI5 보안 완전 가이드 — XSS 방지, CSRF 토큰, CSP 설정부터 XSUAA 연동까지
SAPUI5 보안 완전 가이드 — XSS 방지, CSRF 토큰, CSP 설정부터 XSUAA 연동까지
엔터프라이즈 SAPUI5 애플리케이션에서 반드시 점검해야 할 보안 영역을 체계적으로 다룹니다. 클라이언트 측 XSS 방어부터 서버 측 CSRF 토큰 처리, Content Security Policy 적용, 그리고 SAP BTP 환경의 XSUAA 인증/인가 연동까지 실무 코드와 함께 살펴봅니다.
1. 개요 및 학습 목표
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 인증/인가 흐름을 구성할 수 있다
- 보안 체크리스트를 적용하여 프로덕션 배포 전 취약점을 점검할 수 있다
2. 선수 지식
이 가이드를 효과적으로 따라가려면 아래 항목에 대한 기본적인 이해가 필요합니다.
- SAPUI5 MVC 패턴과 XML View 작성 경험
- OData V2/V4 서비스 바인딩 기본 개념
- HTTP 프로토콜과 헤더(Cookie, Authorization) 기초
- SAP BTP(Business Technology Platform) 계정 및 서브어카운트 구조 이해
- 웹 보안 기초 — OWASP Top 10 항목에 대한 개괄적 인식
3. 환경 / 버전 / 준비물
| 항목 | 권장 사양 |
|---|---|
| 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-inline과 unsafe-eval을 가능한 한 배제하는 것이 권장되지만, SAPUI5 일부 레거시 기능이 eval을 사용하므로 점진적 적용이 현실적입니다.
XSUAA — BTP 환경의 중앙 인증 관리
SAP Authorization and Trust Management Service(XSUAA)는 OAuth 2.0 기반으로 토큰을 발급하고, 앱 라우터(approuter)가 이를 중개합니다. 사용자 브라우저 → approuter → XSUAA → JWT 토큰 발급 → 백엔드 서비스 검증의 흐름을 따릅니다.
5. 실전 코드 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: "<script>alert('XSS')</script>"
});
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 의존도를 낮출 수 있습니다.
6. 흔한 실수 / 트러블슈팅
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.com을 script-src와 style-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 등)만 렌더링하고 나머지는 제거합니다.
7. 다음 단계 / 관련 주제
이 가이드에서 다룬 보안 기초를 확보한 후, 아래 주제로 확장하는 것을 권장합니다.
- 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 터널링 및 접근 제어 설정
8. 참고 자료
- SAPUI5 Security Guide (공식 문서) — UI5 프레임워크의 보안 아키텍처와 권장 사항 종합
- SAPUI5 Content Security Policy (공식 문서) — UI5 앱에서의 CSP 설정 가이드라인
- SAP Authorization and Trust Management Service (XSUAA) — BTP 환경의 인증/인가 서비스 개요
- SAPUI5 공식 문서 메인 — SAPUI5 전체 문서 포털
- SAP BTP Application Router — approuter 설정 및 보안 관련 라우팅 가이드
- OWASP XSS 공격 레퍼런스 — XSS 유형 및 방어 전략 종합
- OWASP CSRF 공격 레퍼런스 — CSRF 공격 메커니즘과 방어 패턴