<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ihopenre-eng.log</title>
        <link>https://velog.io/</link>
        <description>CVE-2026-73842  CVE-2026-88008  CVE-2026-86070   CVE-2026-86069  CVE-2026-73841   CVE-2026-73505  CVE-2026-73840  CVE-2026-73506</description>
        <lastBuildDate>Sun, 16 Aug 2026 07:46:46 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ihopenre-eng.log</title>
            <url>https://velog.velcdn.com/images/ihopenre-eng/profile/c0d30437-9b31-4d02-a459-7f543c77253a/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ihopenre-eng.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ihopenre-eng" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[2026 Gitlab hackathon]]></title>
            <link>https://velog.io/@ihopenre-eng/2026-Gitlab-hackathon</link>
            <guid>https://velog.io/@ihopenre-eng/2026-Gitlab-hackathon</guid>
            <pubDate>Sun, 16 Aug 2026 07:46:46 GMT</pubDate>
            <description><![CDATA[<p>2026 gitlab hackathon  대회에 참여하여 3레벨을 달성하였다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/b586c0a2-bbd5-4d26-b67a-1d5bd9aee017/image.png" alt=""></p>
<p>이번 해커톤을 통해 단순히 프로젝트를 개발하는 것뿐만 아니라, 실제 오픈소스 프로젝트에서 사용하는 Git 기반 협업 과정을 경험해볼 수 있었다.</p>
<p>특히 GitLab을 활용하여 프로젝트를 진행하면서 다음과 같은 작업을 직접 경험했다.</p>
<pre><code>Git을 이용한 소스 코드 관리
Branch를 나누어 기능 개발
Merge Request(PR) 작성 및 관리
다른 사람의 코드 변경사항 확인 및 리뷰
Issue를 통한 작업 내용 공유
Commit 단위로 변경사항 관리
팀원들과 개발 과정에서 발생한 문제에 대해 의견을 주고받는 과정</code></pre><p>평소 개인 프로젝트를 진행할 때는 직접 코드를 작성하고 완성하는 것에 집중했지만, 이번 경험을 통해 여러 사람이 하나의 프로젝트를 개발할 때 코드와 작업 내용을 어떻게 관리해야 하는지 경험할 수 있었다.</p>
<p>특히 Merge Request를 작성하면서 단순히 코드를 올리는 것이 아니라, 어떤 문제를 해결하기 위해 무엇을 변경했는지 다른 개발자가 이해할 수 있도록 설명하는 과정이 중요하다는 것을 알게 되었다.</p>
<p>또한 GitLab의 Issue와 Merge Request를 이용해 작업을 분리하고 변경사항을 관리하면서, 오픈소스 프로젝트에서 이루어지는 협업 방식에 대해서도 직접 경험해볼 수 있었다.</p>
<p>이번 해커톤은 결과적으로 3레벨 달성이라는 성과뿐만 아니라 GitLab 기반의 협업 및 오픈소스 개발 workflow를 직접 경험했다는 점에서 의미가 있었다.</p>
<pre><code>
또한 레벨3 기여자 달성으로 받은 **150credit**으로 머그컵과 노트북 슬리브를 구매하였다.</code></pre><p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/af654dc0-ee1f-40d8-b42a-97b71a13fce3/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[OpenChoreo에서 프로젝트 간 권한 경계를 우회한 원격 명령 실행 취약점 연구(CVE-2026-73841)]]></title>
            <link>https://velog.io/@ihopenre-eng/OpenChoreo%EC%97%90%EC%84%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B0%84-%EA%B6%8C%ED%95%9C-%EA%B2%BD%EA%B3%84%EB%A5%BC-%EC%9A%B0%ED%9A%8C%ED%95%9C-%EC%9B%90%EA%B2%A9-%EB%AA%85%EB%A0%B9-%EC%8B%A4%ED%96%89-%EC%B7%A8%EC%95%BD%EC%A0%90-%EC%97%B0%EA%B5%ACCVE-2026-73841</link>
            <guid>https://velog.io/@ihopenre-eng/OpenChoreo%EC%97%90%EC%84%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B0%84-%EA%B6%8C%ED%95%9C-%EA%B2%BD%EA%B3%84%EB%A5%BC-%EC%9A%B0%ED%9A%8C%ED%95%9C-%EC%9B%90%EA%B2%A9-%EB%AA%85%EB%A0%B9-%EC%8B%A4%ED%96%89-%EC%B7%A8%EC%95%BD%EC%A0%90-%EC%97%B0%EA%B5%ACCVE-2026-73841</guid>
            <pubDate>Sun, 16 Aug 2026 07:35:09 GMT</pubDate>
            <description><![CDATA[<p>OpenChoreo를 분석하던 중 openchoreo-api의 exec 및 wirelogs endpoint에서 Cross-Project Authorization Bypass 취약점을 발견했다.</p>
<p>해당 취약점은 단순한 정보 노출에 그치지 않고, 특정 Project에 대해서만 component:exec 권한을 가진 사용자가 같은 Namespace 내 다른 Project의 Component Pod에서 명령을 실행할 수 있는 문제였다.</p>
<p>해당 취약점은 GitHub Security Advisory GHSA-52gf-6rpq-fgmx로 공개되었으며, 이후 CVE-2026-73841이 할당되었다.</p>
<pre><code>CVE: CVE-2026-73841
GHSA: GHSA-52gf-6rpq-fgmx
Severity: High
Affected Version: 1.2.0-rc.1
Patched Version: 1.2.0
CWE: CWE-639 / CWE-863</code></pre><h1 id="1-취약점이-발생한-이유">1. 취약점이 발생한 이유</h1>
<p>OpenChoreo는 Kubernetes 기반의 개발자 플랫폼이고, 하나의 Namespace 안에 여러 Project가 존재할 수 있다.</p>
<p>예를 들어 다음과 같은 구조를 생각할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/29751f57-c77b-4754-ab73-e6581219f5ce/image.png" alt="">
만약 사용자가 project-a에 대해서만 component:exec 권한을 가지고 있다면 당연히 다음과 같이 동작해야 한다.</p>
<pre><code>project-a/frontend → ALLOW
project-b/backend → DENY
</code></pre><p>그런데 exec endpoint의 authorization 로직을 확인하면서 조금 이상한 부분을 발견했다.</p>
<p>요청에서 project 값을 가져와 authorization에 그대로 사용하고 있었다.</p>
<pre><code>project := query.Get(&quot;project&quot;)</code></pre><p>그리고 이후 authorization 요청을 생성할 때:</p>
<pre><code>Hierarchy: authz.ResourceHierarchy{
    Namespace: namespace,
    Project:   project,
},</code></pre><p>이 값을 그대로 사용한다.</p>
<p>여기서 중요한 점은 이 project 값이 실제 Component의 소유 Project에서 가져온 값이 아니라 HTTP 요청의 query parameter라는 것이다.</p>
<p>즉 사용자가 직접 지정할 수 있다.</p>
<h1 id="2-실제-component는-어떻게-가져오는가">2. 실제 Component는 어떻게 가져오는가?</h1>
<p>그렇다면 실제로 명령을 실행할 대상 Component는 어떻게 가져오는지 확인해봤다.</p>
<p>exec.go에서는 다음과 같은 방식으로 Component를 조회한다.</p>
<pre><code>comp := &amp;openchoreov1alpha1.Component{}

h.k8sClient.Get(
    ctx,
    client.ObjectKey{
        Namespace: namespace,
        Name:      componentName,
    },
    comp,
)
</code></pre><p>여기서 조회 기준은:</p>
<pre><code>Namespace
Component Name</code></pre><p>이다.</p>
<p>문제는 조회한 Component가 앞에서 authorization에 사용했던 Project에 실제로 속해 있는지 확인하지 않는다는 것이다.</p>
<p>예를 들어 공격자가 다음과 같이 요청했다고 가정해보자.</p>
<pre><code>?project=project-a
&amp;component=backend</code></pre><p>authorization에서는:</p>
<pre><code>namespace/acme/project/project-a</code></pre><p>에 대한 권한을 검사한다.</p>
<p>공격자가 <strong>project-a</strong>의 <strong>component:exec</strong> 권한을 가지고 있다면 통과한다.</p>
<p>그 다음 <strong>backend Component</strong>를 조회한다.</p>
<p>그런데 이 <strong>backend</strong>가 실제로 <strong>project-b</strong>에 속해 있어도 이를 확인하지 않는다.</p>
<p>결국 다음과 같은 상황이 만들어진다.</p>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/0a6f5f07-4e2f-4b51-9b82-f0070f615f8e/image.png" alt="">
권한을 검사한 Resource와 실제 접근한 Resource가 서로 다른 것이다.</p>
<h1 id="3-casbin-pdp에서도-문제를-확인할-수-있었다">3. Casbin PDP에서도 문제를 확인할 수 있었다</h1>
<p>여기서 한 가지 더 확인할 부분이 있었다.</p>
<p>OpenChoreo의 authorization은 Casbin 기반 PDP를 사용하고 있는데, PDP에서 실제로 어떤 값을 사용해 authorization decision을 내리는지 확인했다.</p>
<p><strong>internal/authz/casbin/pdp.go</strong>의 <strong>check()</strong>를 확인해보면 다음과 같은 형태였다.</p>
<pre><code>resourcePath := resourceHierarchyToPath(request.Resource.Hierarchy)

result, _ = ce.enforcer.Enforce(
    entitlement,
    resourcePath,
    request.Action,
    ctxJSON,
)</code></pre><p>여기서 authorization에 사용되는 것은 Resource.Hierarchy다.</p>
<p>반면 Resource.ID는 authorization decision에 사용되지 않는다.</p>
<p>즉 다음과 같은 Resource가 들어온다고 해보자.</p>
<pre><code>Resource.ID = victim-svc

Hierarchy:
    Namespace = acme
    Project   = project-a</code></pre><p><strong>authorization engine</strong> 입장에서는 사실상:</p>
<pre><code>acme/project-a</code></pre><p>에 대한 권한을 검사하게 된다.</p>
<p><strong>victim-svc</strong>가 실제로** project-b**에 속해 있는지는 이 단계에서 확인되지 않는다.</p>
<p>이 부분이 상당히 중요했다.</p>
<h1 id="4-실제로-poc를-만들어봤다">4. 실제로 PoC를 만들어봤다</h1>
<p>코드 분석만으로 끝내지 않고 OpenChoreo에서 실제 사용하는 Casbin PDP를 대상으로 간단한 테스트를 작성했다.</p>
<p>공격자에게는 다음 권한만 부여했다.</p>
<pre><code>Project: project-a
Action:  component:exec</code></pre><p>피해 Component는:</p>
<pre><code>Project: project-b
Component: victim-svc</code></pre><p>로 설정했다.</p>
<p>먼저 정상적인 요청처럼 피해 Component의 실제 Project인 project-b를 기준으로 authorization을 수행했다.</p>
<pre><code>Namespace = acme
Project   = project-b
Component = victim-svc</code></pre><p>결과는:</p>
<pre><code>Decision = false</code></pre><p>예상한 결과다.</p>
<p>공격자는 project-b에 권한이 없기 때문이다.</p>
<p>그런데 exec.go에서 실제로 생성하는 authorization request와 동일한 형태로 테스트해봤다.</p>
<pre><code>Namespace = acme
Project   = project-a
Component = unset

Resource.ID = victim-svc
</code></pre><p>결과는:</p>
<pre><code>Decision = true</code></pre><p>였다.</p>
<p>즉 실제 대상은:</p>
<pre><code>acme/project-b/victim-svc</code></pre><p>인데 authorization은:</p>
<pre><code>acme/project-a</code></pre><p>를 기준으로 통과한 것이다.</p>
<p>이 테스트를 통해 단순한 코드상의 의심이 아니라 실제 authorization engine에서도 동일한 문제가 재현되는 것을 확인했다.</p>
<h1 id="5-실제-공격-흐름">5. 실제 공격 흐름</h1>
<p>이제 전체 흐름을 정리하면 상당히 간단하다.</p>
<p>공격자는 <strong>project-a</strong>에 대한 정상적인 <strong>component:exec</strong> 권한을 가지고 있다고 가정한다.</p>
<p>피해자는 같은** Namespace<strong>의 **project-b</strong>에서 <strong>victim-svc</strong>를 실행하고 있다.</p>
<p>공격자는 다음과 같이 요청할 수 있다.</p>
<pre><code>GET /exec/namespaces/acme/components/victim-svc?project=project-a&amp;env=dev&amp;tty=true&amp;stdin=true&amp;command=sh
Authorization: Bearer &lt;attacker-token&gt;
Upgrade: websocket</code></pre><p>서버 내부에서는 대략 다음과 같이 동작한다.</p>
<pre><code>1. project-a ← 공격자가 지정
        ↓
2. project-a에 대한 exec 권한 확인
        ↓
3. 권한 있음 → ALLOW
        ↓
4. victim-svc 조회
        ↓
5. 실제 owner = project-b
        ↓
6. 하지만 project-a == project-b 검증 없음
        ↓
7. victim-svc Pod에 exec
</code></pre><p>따라서 공격자는 자신의 Project에 대한 정당한 권한을 이용해 다른 Project의 workload까지 접근할 수 있다.</p>
<h1 id="6-impact">6. Impact</h1>
<p>이 취약점의 가장 큰 문제는 단순히 다른 Project의 Component 정보를 조회하는 수준이 아니라는 점이다.</p>
<p><strong>exec endpoint</strong>를 이용하면 대상** Pod**에서 명령을 실행할 수 있다.</p>
<p>따라서 공격자는 해당 <strong>workload</strong>의 환경변수나** mounted Secret** 등에 접근할 수 있다.</p>
<p>예를 들어 다음과 같은 정보가** workload** 내부에 존재한다면:</p>
<pre><code>DATABASE_URL
API_KEY
ACCESS_TOKEN
SECRET
</code></pre><p>이를 읽을 가능성이 있다.</p>
<p>또한 workload의 권한과 network 위치를 이용해 같은 내부 환경에 존재하는 다른 서비스로 추가적인 접근을 시도할 수도 있다.</p>
<p>즉 공격 시나리오는 단순한:</p>
<p><strong>Project A → Project B</strong> 정보 조회</p>
<p>가 아니라:</p>
<pre><code>Project A 권한
      ↓
Authorization Bypass
      ↓
Project B Component
      ↓
Pod Command Execution
      ↓
Secrets / Credentials
      ↓
Potential Lateral Movement</code></pre><p>까지 이어질 수 있다.</p>
<h1 id="7-namespace-경계는-넘어가지-않는다">7. Namespace 경계는 넘어가지 않는다</h1>
<p>다만 이 취약점의 범위를 정확하게 이해할 필요가 있다.</p>
<p>이 문제는 Namespace를 기준으로 한 authorization 자체를 우회하는 것은 아니다.</p>
<p>예를 들어:</p>
<pre><code>namespace: acme

project-a ← attacker
project-b ← victim</code></pre><p>에서는 문제가 발생한다.</p>
<p>하지만:</p>
<pre><code>namespace: acme
        ↓
namespace: production</code></pre><p>과 같이 다른 Namespace로 이동할 수 있는 취약점은 아니다.</p>
<p>따라서 이번 취약점의 핵심은 Namespace 내부의 Project 간 isolation이 깨진 것이라고 보는 것이 정확하다.</p>
<h1 id="8-wirelogs에서도-동일한-문제가-있었다">8. wirelogs에서도 동일한 문제가 있었다</h1>
<p>exec만 확인하고 끝내지 않고 비슷한 구조를 사용하는 다른 endpoint도 확인했다.</p>
<p>그 과정에서 <strong>wirelogs</strong>에서도 동일한 <strong>root cause</strong>를 발견했다.</p>
<p><strong>wirelogs</strong> 역시 요청에서 전달된 <strong>Project/Component</strong> 정보를 기반으로** authorization<strong>을 수행하고 있었고, 실제 **Component</strong>의 <strong>owner Project</strong>와 요청에 포함된 Project가 동일한지 검증하지 않았다.</p>
<p>결과적으로 공격자는 자신에게** logs:view** 권한이 있는 Project를 지정해 같은 <strong>Namespace</strong>에 존재하는 다른 Project의 <strong>Component wirelogs</strong>를 조회할 수 있었다.</p>
<p>결국 하나의** authorization **설계 문제가:</p>
<pre><code>exec
 └── Cross-Project Command Execution

wirelogs
 └── Cross-Project Log Disclosure
</code></pre><p>두 가지 문제로 이어지고 있었다.</p>
<h1 id="9-다른-api와-비교하면서-원인을-확실히-알-수-있었다">9. 다른 API와 비교하면서 원인을 확실히 알 수 있었다</h1>
<p>이후 <strong>OpenChoreo</strong>의 다른 <strong>authorization</strong> 코드들도 확인했다.</p>
<p>특히** Component** 관련 다른 <strong>operation</strong>에서는 먼저 실제 Component를 가져온 다음:</p>
<pre><code>component.Spec.Owner.ProjectName</code></pre><p>을 이용해 <strong>authorization hierarchy</strong>를 구성하고 있었다.</p>
<p>즉 정상적인 코드는:</p>
<pre><code>Request
   ↓
Fetch Component
   ↓
Read Owner Project
   ↓
Authorization
   ↓
Action</code></pre><p>형태였다.</p>
<p>반면 취약했던 <strong>exec</strong>와 <strong>wirelogs</strong>는:</p>
<pre><code>Request
   ↓
Read project from query
   ↓
Authorization
   ↓
Fetch Component
   ↓
Action</code></pre><p>순서였다.</p>
<p>이 차이가 이번 취약점의 핵심적인 원인이라고 판단했다.</p>
<h1 id="10-어떻게-수정해야-하는가">10. 어떻게 수정해야 하는가?</h1>
<p>해결 방법 자체는 비교적 명확하다.</p>
<p><strong>authorization</strong>에 사용되는 Project를 사용자의 입력에서 가져오면 안 된다.</p>
<p>먼저 실제 Component를 조회하고:</p>
<pre><code>comp := &amp;openchorev1alpha1.Component{}

h.k8sClient.Get(
    ctx,
    client.ObjectKey{
        Namespace: namespace,
        Name:      componentName,
    },
    comp,
)</code></pre><p>실제 owner를 확인해야 한다.</p>
<pre><code>comp.Spec.Owner.ProjectName</code></pre><p>그리고 authorization을 수행할 때 실제 owner를 사용한다.</p>
<pre><code>Hierarchy: authz.ResourceHierarchy{
    Namespace: namespace,
    Project:   comp.Spec.Owner.ProjectName,
    Component: comp.Name,
},</code></pre><p>이렇게 하면 사용자가:</p>
<pre><code>?project=project-a</code></pre><p>를 전달하더라도 서버가 authorization을 수행하는 기준은 실제 Component의 owner인 project-b가 된다.</p>
<p>결과적으로:</p>
<pre><code>Attacker
   ↓
project-a 권한
   ↓
victim-svc
   ↓
실제 owner = project-b
   ↓
project-b 권한 검사
   ↓
DENY
</code></pre><p>가 된다.</p>
<h1 id="11-패치">11. 패치</h1>
<p>OpenChoreo 측에서는 취약점을 확인한 이후 수정 작업을 진행했고, 관련 수정 PR이 공개되었다.</p>
<p>수정된 버전에서는 exec와 wirelogs 역시 다른 Component operation과 동일하게 실제 Resource를 먼저 조회하고, Resource의 ownership 정보를 기반으로 authorization을 수행하도록 변경되었다.</p>
<p>영향받는 버전은:</p>
<pre><code>1.2.0-rc.1</code></pre><p>이며 패치 버전은:</p>
<pre><code>1.2.0</code></pre><p>이다.</p>
<ol start="12">
<li>마치며</li>
</ol>
<p>이번 취약점을 분석하면서 가장 인상적이었던 부분은 취약점 자체가 매우 복잡한 코드에서 발생한 것이 아니라는 점이다.</p>
<p>문제의 핵심은 결국 다음과 같았다.</p>
<pre><code>사용자가 전달한 Project
        ↓
Authorization

실제 Component의 Project
        ↓
검증하지 않음</code></pre><p>Authorization 로직에서는 단순히 &quot;권한 체크가 존재하는가&quot;만 확인해서는 부족하다.</p>
<p>실제로는 다음과 같은 부분을 같이 확인해야 한다.</p>
<pre><code>어떤 Resource를 대상으로 권한을 검사하는가?

그 Resource의 ID는 어디에서 오는가?

Project / Namespace 등의 hierarchy는 누가 결정하는가?

사용자 입력이 authorization context에 직접 들어가는가?

Authorization 대상과 실제 Action 대상이 동일한가?</code></pre><p>특히 <strong>multi-tenant</strong> 환경에서는 사용자가 주장하는 <strong>resource scope</strong>를 그대로 신뢰하지 않고, 실제 <strong>Resource</strong>의 <strong>identity</strong>와 <strong>ownership</strong>에서 <strong>authorization context</strong>를 파생시키는 것 이 중요하다.</p>
<p>이번 OpenChoreo 취약점도 결국 이 부분이 분리되면서 발생했다.</p>
<p>하나의 Project에 대한 정상적인 권한이 다른 Project의 Component에 대한 권한으로 확장될 수 있었고, 그 결과 Namespace 내부의 Project isolation이 무너졌다.</p>
<h1 id="references">References</h1>
<pre><code>OpenChoreo: https://github.com/openchoreo/openchoreo
GHSA-52gf-6rpq-fgmx
CVE-2026-73841
OpenChoreo Fix PR #4251</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Oh My Posh Path Template Injection을 통한 Arbitrary Command Execution 분석 (CVE-2026-73505)]]></title>
            <link>https://velog.io/@ihopenre-eng/Oh-My-Posh-Path-Template-Injection%EC%9D%84-%ED%86%B5%ED%95%9C-Arbitrary-Command-Execution-%EB%B6%84%EC%84%9D-GHSA-6xj8-qv9j-xcjq</link>
            <guid>https://velog.io/@ihopenre-eng/Oh-My-Posh-Path-Template-Injection%EC%9D%84-%ED%86%B5%ED%95%9C-Arbitrary-Command-Execution-%EB%B6%84%EC%84%9D-GHSA-6xj8-qv9j-xcjq</guid>
            <pubDate>Fri, 31 Jul 2026 08:57:53 GMT</pubDate>
            <description><![CDATA[<pre><code>Severity: High
Reporter: ihopenre-eng
Published: July 2026</code></pre><h1 id="introduction">Introduction</h1>
<p>최근 제가 Oh My Posh 프로젝트에서 보고한 취약점이 GitHub Security Advisory(CVE-2026-73505)로 공개되었습니다.</p>
<p>이번 취약점은 단순한 템플릿 처리 문제가 아니라, 사용자가 현재 위치한 디렉터리 이름을 다시 Go text/template 엔진으로 렌더링하는 과정에서 발생했습니다.</p>
<p>디렉터리 이름에 Go Template 표현식을 포함하면, 프롬프트가 렌더링되는 시점에 해당 표현식이 실행됩니다. 특히 템플릿 함수(cmd)를 통해 운영체제 명령을 실행할 수 있어 현재 사용자 권한으로 임의 명령 실행(Arbitrary Command Execution) 이 가능했습니다.</p>
<p>이번 글에서는 취약점을 분석하면서 어떤 코드가 원인이 되었는지, 실제로 어떻게 명령 실행으로 이어졌는지 살펴보겠습니다.</p>
<h1 id="oh-my-posh는-어떻게-path를-표시할까">Oh My Posh는 어떻게 Path를 표시할까?</h1>
<p>여기서는 먼저 Path Segment가 어떻게 생성되는지 설명합니다.</p>
<h2 id="prompt-렌더링-구조도">Prompt 렌더링 구조도</h2>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/b6e7fe0b-effb-4b37-b57b-a1cfe99c0c93/image.png" alt=""></p>
<p>처음 의심했던 부분</p>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/a0b1a24b-daab-49f2-9339-0707a6c358a2/image.png" alt="">
처음에는 이 코드가 단순히 설정 파일에 포함된 템플릿만 처리하는 것으로 생각했습니다.</p>
<p>하지만 pt.Path가 어디에서 만들어지는지 계속 따라가 보니 조금 다른 모습을 확인할 수 있었습니다.</p>
<h1 id="ptpath는-어디에서-오는가">pt.Path는 어디에서 오는가?</h1>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/adc7180a-576d-4d51-8263-4af5c62dc790/image.png" alt="">
folder name이</p>
<pre><code>fmt.Sprintf(folderFormat, element)</code></pre><p>로 들어가는 부분.</p>
<p>element는 현재 작업 디렉터리의 실제 폴더 이름입니다.</p>
<p>즉,</p>
<pre><code>hello

도 들어가고

{{ cmd `whoami` }}

같은 문자열도 그대로 들어갑니다.</code></pre><h1 id="문제는-두-번째-template-rendering">문제는 &quot;두 번째 Template Rendering&quot;</h1>
<p>여기가 핵심입니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/b3b0a399-e418-494f-b823-e22534f12331/image.png" alt=""></p>
<p>여기서</p>
<blockquote>
<p>이미 사용자 입력이 포함된 문자열을 다시 Template Engine으로 해석하고 있었습니다.</p>
</blockquote>
<h1 id="cmd-함수는-무엇을-할까">cmd 함수는 무엇을 할까?</h1>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/95b36a42-9f00-4275-845c-214c11fe99f2/image.png" alt=""></p>
<p>템플릿 엔진에는 여러 함수가 등록되어 있는데,</p>
<p>그중 cmd()는 운영체제 명령을 실행하는 함수입니다.</p>
<p>즉,</p>
<pre><code>{{ cmd &quot;whoami&quot; }}</code></pre><p>가 Template 안에서 실제 명령을 실행합니다.</p>
<h1 id="왜-이것이-rce가-되는가">왜 이것이 RCE가 되는가?</h1>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/2d1fffec-1158-454f-bf03-c844b5c3be4f/image.png" alt="">
이 그림 하나 있으면 이해가 엄청 쉬워집니다.</p>
<h1 id="poc">PoC</h1>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/de216eec-15e7-43d9-8bb3-e12dc04e46eb/image.png" alt=""></p>
<p>프롬프트를 렌더링하는 순간</p>
<pre><code>whoami</code></pre><p>명령이 실행되고</p>
<p>출력이 Prompt에 그대로 반영됩니다.</p>
<h1 id="windows-검증">Windows 검증</h1>
<pre><code>powershell</code></pre><p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/a703b62e-9e35-4d1d-8651-7aafa406f173/image.png" alt="">
실행 후</p>
<pre><code>pwn3d</code></pre><p>파일 생성.</p>
<p>영향</p>
<p>여기서 실제 공격 시나리오.</p>
<pre><code>Git Clone
ZIP 압축
USB
Network Share</code></pre><p>사용자가</p>
<pre><code>cd</code></pre><p>하는 순간</p>
<pre><code>Prompt Rendering

↓

Command Execution</code></pre><p>패치</p>
<p>패치 Diff.</p>
<p>여기서</p>
<pre><code>Template.Render 제거</code></pre><p>또는</p>
<pre><code>Escape</code></pre><p>되는 부분.</p>
<h1 id="마무리">마무리</h1>
<p>이 취약점은 Go Template 자체의 문제가 아니라 신뢰할 수 없는 데이터를 Template로 다시 해석(Double Evaluation) 한 것이 원인이었습니다.</p>
<p>템플릿 엔진는 설정 파일처럼 신뢰할 수 있는 입력을 처리할 때는 매우 유용하지만, 사용자나 파일시스템에서 가져온 문자열을 그대로 다시 렌더링하면 의도하지 않은 코드 실행으로 이어질 수 있습니다.</p>
<p>이번 사례는 &quot;데이터와 템플릿을 명확히 구분해야 한다&quot;는 기본 원칙이 얼마나 중요한지를 보여주는 사례라고 생각합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[OpenChoreo Cluster Gateway 인증 부재 취약점 분석 (CVE-2026-73842)]]></title>
            <link>https://velog.io/@ihopenre-eng/OpenChoreo-Cluster-Gateway-%EC%9D%B8%EC%A6%9D-%EB%B6%80%EC%9E%AC-%EC%B7%A8%EC%95%BD%EC%A0%90-%EB%B6%84%EC%84%9D-GHSA-rh53-xvx2-j327</link>
            <guid>https://velog.io/@ihopenre-eng/OpenChoreo-Cluster-Gateway-%EC%9D%B8%EC%A6%9D-%EB%B6%80%EC%9E%AC-%EC%B7%A8%EC%95%BD%EC%A0%90-%EB%B6%84%EC%84%9D-GHSA-rh53-xvx2-j327</guid>
            <pubDate>Fri, 31 Jul 2026 07:37:02 GMT</pubDate>
            <description><![CDATA[<h3 id="severity-critical">Severity: Critical</h3>
<p>Reporter: ihopenre-eng
Published: July 2026</p>
<h1 id="introduction">Introduction</h1>
<p>최근 제가 OpenChoreo 프로젝트에서 보고한 취약점이 GitHub Security Advisory(CVE-2026-73842)로 공개되었습니다.</p>
<p>이번 취약점은 Cluster Gateway의 내부 API가 호출자를 인증하지 않는 설계 문제에서 시작됩니다. 내부 네트워크에서 Gateway에 접근할 수 있는 환경이라면 Kubernetes API로 전달되는 요청에 대해 별도의 인증이나 권한 검사가 이루어지지 않아 Secret 조회, 워크로드 변경, Pod 내부 명령 실행과 같은 동작이 가능했습니다.</p>
<p>이번 글에서는 취약점을 분석하면서 어떤 코드가 원인이 되었는지, 그리고 이러한 설계가 실제로 어떤 영향을 미칠 수 있는지 순서대로 살펴보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/f5a1c75e-c530-43f0-b8b4-b608bd8c6fc2/image.png" alt=""></p>
<h1 id="openchoreo-구조-살펴보기">OpenChoreo 구조 살펴보기</h1>
<p>코드를 보기 전에 먼저 OpenChoreo의 구조를 간단히 이해하면 이후 내용을 따라가기 훨씬 쉽습니다.</p>
<p>OpenChoreo는 Control Plane이 여러 Data Plane Kubernetes Cluster를 관리하는 구조입니다. 이때 Cluster Gateway는 Control Plane과 Data Plane 사이에서 Kubernetes API 요청을 전달하는 역할을 합니다.</p>
<p>즉, Gateway는 여러 클러스터를 연결하는 핵심 구성 요소라고 볼 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/e06342fe-5e6f-4a4a-95b2-09a760bcea5f/image.png" alt=""></p>
<p>이 구조를 이해한 상태에서 자연스럽게 이런 질문이 생겼습니다.</p>
<blockquote>
<p>&quot;Gateway는 내부 요청을 어떻게 인증할까?&quot;</p>
</blockquote>
<p>이 질문을 시작으로 코드를 따라가 보았습니다.</p>
<h1 id="internal-api는-어떻게-보호되고-있을까">Internal API는 어떻게 보호되고 있을까?</h1>
<h4 id="먼저-gateway에서-어떤-api를-제공하는지-확인했습니다">먼저 Gateway에서 어떤 API를 제공하는지 확인했습니다.</h4>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/1342b38c-3e00-47ca-bc3d-e86f3254fc59/image.png" alt=""></p>
<p>코드를 보면 /api/proxy/, /api/exec/, /api/wirelogs/ 세 가지 Internal API가 등록되어 있습니다.</p>
<pre><code>internalMux.HandleFunc(&quot;/api/proxy/&quot;, ...)
internalMux.HandleFunc(&quot;/api/exec/&quot;, ...)
internalMux.HandleFunc(&quot;/api/wirelogs/&quot;, ...)</code></pre><p>여기서 가장 궁금했던 점은 &quot;이 요청을 누가 인증하는가?&quot;였습니다.</p>
<p>일반적으로 이러한 관리용 API는 인증 미들웨어를 거치거나, 클라이언트 인증서를 검증하는 코드가 먼저 등장합니다.</p>
<p>하지만 코드를 계속 따라가도 그런 처리를 찾을 수 없었습니다.</p>
<blockquote>
<p>오히려 눈에 띈 것은 TLS 설정이었습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/ebad0c3c-8532-4bfd-bf0d-eacd2099acac/image.png" alt=""></p>
<pre><code>tlsConfig := &amp;tls.Config{
    ClientAuth: tls.RequestClientCert,
}</code></pre><p>처음에는 이 설정을 보고 &quot;클라이언트 인증서를 사용해 접근을 제어하는구나.&quot;라고 생각했습니다. 하지만 RequestClientCert는 이름 그대로 클라이언트 인증서를 요청(Request) 할 뿐, 인증서가 반드시 있어야 하는 것도 아니고 유효한지까지 검증하는 설정도 아닙니다.</p>
<p>즉, TLS 연결은 암호화되지만 이 연결을 요청한 클라이언트가 신뢰할 수 있는 대상인지까지는 확인하지 않습니다.</p>
<p>그래서 자연스럽게 다음 단계로 넘어갔습니다.</p>
<p>&quot;그렇다면 Handler 내부에서 별도로 인증을 수행하지 않을까?&quot;</p>
<h1 id="handler에서는-인증을-수행할까">Handler에서는 인증을 수행할까?</h1>
<p>이번에는 handleHTTPProxy(), handleExec(), handleWirelogs()를 하나씩 따라가 보았습니다.</p>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/5f157fcf-1ee3-4fae-93a6-6220d0bb1500/image.png" alt=""></p>
<p>관리용 API라면 일반적으로 Handler에 들어오기 전에 인증 미들웨어를 거치거나, Handler 내부에서 클라이언트 인증서나 토큰을 검증하는 코드가 존재합니다.</p>
<p>예를 들어 다음과 같은 처리를 기대했습니다.</p>
<pre><code>r.TLS.PeerCertificates를 이용한 클라이언트 인증서 검증
JWT 또는 Access Token 검증
호출자의 권한(Role) 확인
특정 Data Plane에 접근 가능한 사용자 여부 확인
</code></pre><p>하지만 handleHTTPProxy()를 비롯한 관련 Handler에서는 이러한 인증 과정을 찾을 수 없었습니다.</p>
<p>즉, 요청이 Internal API에 도달하면 호출자가 누구인지 확인하는 절차 없이 그대로 다음 단계로 전달되고 있었습니다.</p>
<p>여기서 한 가지 의문이 생겼습니다.</p>
<blockquote>
<p>&quot;인증은 없더라도 최소한 요청 자체는 엄격하게 제한하고 있지 않을까?&quot;</p>
</blockquote>
<p>이 질문을 확인하기 위해 이번에는 요청을 검사하는 RequestValidator를 분석해 보았습니다.</p>
<h1 id="requestvalidator는-어떤-요청을-허용할까">RequestValidator는 어떤 요청을 허용할까?</h1>
<p>handleHTTPProxy()를 따라가다 보면 최종적으로 RequestValidator를 통해 요청을 검사하는 것을 확인할 수 있습니다.</p>
<p>처음에는 이 Validator가 이름 그대로 읽기 전용(Read-only) 요청만 허용할 것으로 예상했습니다. 관리용 프록시라면 일반적으로 GET이나 일부 조회 API만 허용하고, 리소스를 변경하는 요청은 차단하는 것이 일반적이기 때문입니다.</p>
<p>그래서 Validator가 어떤 HTTP Method를 허용하는지 먼저 살펴보았습니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/6a763b2c-095e-4942-970a-b50f042340d2/image.png" alt=""></p>
<p>코드를 확인한 결과 예상과는 조금 달랐습니다.</p>
<p>GET뿐만 아니라 POST, PUT, PATCH, DELETE까지 모두 허용하고 있었습니다.</p>
<p>즉, 이 프록시는 단순히 Kubernetes API를 조회하는 용도가 아니라 리소스를 생성하거나 수정, 삭제하는 요청도 그대로 전달할 수 있는 구조였습니다.</p>
<p>반면 클라이언트 라이브러리에는 해당 프록시가 &quot;Read-only&quot;라는 설명이 포함되어 있었기 때문에, 실제 구현은 Client Library의 설명보다 더 넓은 범위의 요청을 허용하고 있었습니다.</p>
<h1 id="secret-접근-제한은-충분할까">Secret 접근 제한은 충분할까?</h1>
<p>다음으로 확인한 부분은 어떤 Kubernetes API 경로를 차단하는지였습니다.</p>
<p>Validator에는 일부 민감한 API를 차단하기 위한 Block List가 존재합니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/5ce32100-5edc-468c-88c2-cc8d9d88129e/image.png" alt=""></p>
<pre><code>/api/v1/namespaces/kube-system/secrets</code></pre><p>처음에는 모든 Secret API가 차단될 것으로 예상했지만, 실제로는 kube-system 네임스페이스의 Secret만 예외적으로 차단하고 있었습니다.</p>
<p>즉,</p>
<pre><code>/api/v1/namespaces/default/secrets
/api/v1/namespaces/production/secrets
/api/v1/namespaces/tenant-a/secrets</code></pre><p>와 같은 일반 Tenant Namespace의 Secret 요청은 Validator를 통과할 수 있었습니다.</p>
<p>이러한 Secret에는 다음과 같은 민감한 정보가 저장되는 경우가 많습니다.</p>
<pre><code>Database 계정 정보
Cloud Access Key
TLS Private Key
API Token
Kubernetes ServiceAccount Token</code></pre><p>만약 공격자가 이러한 Secret을 읽을 수 있다면, 단순히 Kubernetes 리소스를 조회하는 수준을 넘어 다른 시스템으로의 추가적인 공격도 가능해질 수 있습니다.</p>
<h1 id="실제로-어떤-영향을-미칠-수-있을까">실제로 어떤 영향을 미칠 수 있을까?</h1>
<p>지금까지 확인한 내용을 정리하면 다음과 같습니다.</p>
<pre><code>Internal API에 대한 호출자 인증이 없다.
Data Plane에 대한 권한 검사가 없다.
쓰기(Write) 요청도 허용된다.
Tenant Namespace의 Secret 조회도 가능하다.</code></pre><p>이 네 가지가 결합되면서 Gateway는 Kubernetes API 요청을 중계하는 과정에서
호출자 인증이나 권한 검사를 수행하지 않는 구조였습니다.</p>
<p>공격자가 Internal API에 접근할 수 있는 환경이라면 다음과 같은 작업을 수행할 수 있습니다.</p>
<pre><code>Secret 조회
Deployment 생성 및 삭제
ConfigMap 수정
Service 변경
Pod 내부 명령 실행(/api/exec/)</code></pre><p>특히 Cluster Gateway는 여러 Data Plane을 관리하는 구조이기 때문에, 하나의 Gateway를 통해 여러 Kubernetes Cluster에 영향을 줄 수 있다는 점도 중요한 부분입니다.</p>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/a5fdc239-ef71-4d61-8279-a91cbe9b121e/image.png" alt=""></p>
<h1 id="재현poc">재현(POC)</h1>
<p>이번 취약점은 실제 Kubernetes 환경을 구성하지 않더라도 RequestValidator만으로 동작을 확인할 수 있었습니다.</p>
<p>이를 위해 Validator를 대상으로 간단한 단위 테스트를 작성했습니다.</p>
<blockquote>
<p>아래 테스트는 Validator가 어떤 요청을 허용하는지 확인하기 위해 작성한 PoC입니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/bf146a9c-7aa8-4b08-b45a-01a94dc522a2/image.png" alt=""></p>
<p>테스트에서는 다음과 같은 항목을 확인했습니다.</p>
<p>Tenant Namespace Secret 조회 요청이 허용되는지
DELETE 요청이 허용되는지
PATCH 요청이 허용되는지</p>
<p>모든 테스트는 예상한 결과대로 통과했으며, Validator가 이러한 요청을 차단하지 않는다는 것을 확인할 수 있었습니다.</p>
<p>이후 실제 요청 흐름을 분석해 보니, Validator를 통과한 요청은 Gateway를 거쳐 Data Plane Agent로 전달되고, 최종적으로 Kubernetes API Server에서 그대로 실행되는 구조였습니다.</p>
<h1 id="마무리">마무리</h1>
<p>이번 취약점은 하나의 코드 실수라기보다는 여러 보안 검증이 동시에 빠져 있었던 설계상의 문제에 가까웠습니다.</p>
<p>Gateway는 내부 관리용 서비스라는 이유로 호출자를 신뢰하고 있었지만, 현대적인 클라우드 환경에서는 내부 네트워크 역시 항상 신뢰할 수 있다고 가정하기 어렵습니다.</p>
<p>따라서 관리용 API라고 하더라도 다음과 같은 보안 계층은 반드시 필요합니다.</p>
<pre><code>호출자 인증(Authentication)
대상 리소스에 대한 권한 검증(Authorization)
최소 권한 원칙(Least Privilege)
네트워크 접근 제어(Network Policy)</code></pre><p>이번 분석을 통해 다시 한 번 느낀 점은, &quot;내부 서비스이기 때문에 안전하다&quot;는 가정은 더 이상 유효하지 않다는 것입니다. Zero Trust 환경에서는 내부 API 역시 외부 서비스와 동일한 수준의 인증과 권한 검사를 적용해야 하며, 그렇지 않을 경우 하나의 Gateway가 전체 Kubernetes 환경의 공격 지점이 될 수 있습니다.</p>
<h2 id="references">References</h2>
<ul>
<li>GitHub Security Advisory
<a href="https://github.com/openchoreo/openchoreo/security/advisories/GHSA-rh53-xvx2-j327">https://github.com/openchoreo/openchoreo/security/advisories/GHSA-rh53-xvx2-j327</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[스터디 카페 앱 제작 #5]]></title>
            <link>https://velog.io/@ihopenre-eng/%EC%8A%A4%ED%84%B0%EB%94%94-%EC%B9%B4%ED%8E%98-%EC%95%B1-%EC%A0%9C%EC%9E%91-5</link>
            <guid>https://velog.io/@ihopenre-eng/%EC%8A%A4%ED%84%B0%EB%94%94-%EC%B9%B4%ED%8E%98-%EC%95%B1-%EC%A0%9C%EC%9E%91-5</guid>
            <pubDate>Wed, 15 Jul 2026 07:19:58 GMT</pubDate>
            <description><![CDATA[<p>4편에서는 결제 증빙, 알림, 채팅, PWA, 보안처럼 실제 운영을 위해 다듬었던 부분을 정리해봤습니다. 이번에는 앱 기능 자체보다, 만든 앱을 실제 서비스처럼 공개하기 위해 했던 부가 작업들을 적어보려고 합니다.</p>
<p>처음에는 앱만 잘 만들면 끝이라고 생각했습니다. 예약도 되고, 관리자 페이지도 있고, 모바일에서도 어느 정도 동작하면 바로 쓸 수 있을 줄 알았습니다. 그런데 막상 외부에서 접속할 수 있게 만들려고 하니까 로그인, 도메인, 배포, 검색 색인 같은 작업이 따로 필요했습니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/802624d1-4070-4acc-a651-727baf2c1ce3/image.png" alt=""></p>
<p>먼저 구글 로그인을 붙여봤습니다. 일반 회원가입만 있어도 되긴 했지만, 사용자가 매번 이메일과 비밀번호를 입력하는 것보다 구글 계정으로 바로 로그인하는 게 더 편할 것 같았습니다. 그래서 Google OAuth를 추가해봤습니다.</p>
<p>처음에는 버튼만 붙이면 바로 될 줄 알았습니다. 그런데 실제로 해보니까 생각보다 잘 안 됐습니다. 로컬에서는 되는 것 같은데 배포 환경에서는 안 되거나, 버튼이 제대로 렌더링되지 않거나, 모바일 브라우저에서 버튼이 잘리는 문제가 있었습니다. 특히 Google Identity Services는 등록된 도메인과 redirect 설정이 맞지 않으면 바로 막혀서, 승인된 origin과 callback 흐름을 계속 확인해야 했습니다.</p>
<p>그래서 구글 로그인은 단순히 프론트에 버튼을 붙이는 작업이 아니라, 서버에서 토큰을 검증하고, 배포 도메인을 OAuth 설정에 맞추고, 브라우저별 렌더링 문제까지 같이 봐야 했습니다. 처음에는 로그인 버튼 UI 문제인 줄 알았는데, 실제로는 OAuth 설정, 쿠키, 도메인, 브라우저 정책이 다 연결되어 있었습니다.</p>
<p>카카오 로그인도 추가해봤습니다. 한국 사용자에게는 카카오 로그인이 익숙하기 때문에 넣어두면 접근성이 좋아질 것 같았습니다. 그런데 카카오 로그인도 redirect URI 때문에 많이 막혔습니다. 개발할 때 쓰는 주소와 실제 배포 주소가 다르면 로그인이 실패했고, callback 이후 세션을 앱으로 넘기는 부분도 맞춰야 했습니다.</p>
<p>처음에는 redirect URI를 고정값처럼 넣어봤는데, 배포 환경이 바뀌면 다시 문제가 생겼습니다. 그래서 실제 요청이 들어온 host를 기준으로 redirect URI를 처리하도록 수정했습니다. 이렇게 바꾸니까 로컬과 배포 환경 차이 때문에 생기던 문제를 줄일 수 있었습니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/758a70ae-52c9-4436-9274-5b2da8425a96/image.png" alt=""></p>
<p>도메인 연결도 해봤습니다. 배포 플랫폼에서 제공하는 기본 주소로도 접속은 가능했지만, 실제 서비스처럼 보이려면 도메인이 필요했습니다. 도메인을 연결하면서 DNS 설정, HTTPS 적용, 프론트와 백엔드 주소 분리 같은 부분을 확인했습니다. 이때부터는 “코드가 동작한다”와 “서비스처럼 접속된다”가 다른 문제라는 걸 느꼈습니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/ce796d9c-8db1-4cd4-b969-b4d1c98b51a2/image.png" alt=""></p>
<p>검색 노출도 신경 써봤습니다. 앱을 만들었는데 검색했을 때 아무것도 안 나오면 외부에서 찾기 어렵기 때문에 Google Search Console에 등록해봤습니다. 처음에는 그냥 사이트 주소만 넣으면 되는 줄 알았는데, 실제로는 소유권 확인, sitemap.xml, robots.txt, 색인 요청 같은 작업이 필요했습니다.</p>
<p>소유권 확인을 위해 구글에서 제공하는 인증 파일을 추가하고, 검색 엔진이 어떤 페이지를 읽어야 하는지 알려주기 위해 sitemap.xml도 만들었습니다. 그리고 검색을 막지 않도록 robots.txt도 정리했습니다. 처음에는 이런 파일들이 별로 중요하지 않을 줄 알았는데, 검색 노출을 하려면 기본 설정이 꽤 중요했습니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/e0d3dbf3-3615-4cac-83a1-4ab57eecb8c2/image.png" alt=""></p>
<p>색인 작업을 하면서도 바로 검색 결과에 뜨지는 않았습니다. 등록했다고 즉시 반영되는 게 아니라, 구글이 페이지를 크롤링하고 색인할 때까지 시간이 필요했습니다. 그래서 Search Console에서 색인 요청을 하고, 사이트맵이 정상적으로 읽히는지 확인했습니다. 이 과정에서 앱도 결국 웹 서비스이기 때문에 검색 엔진이 이해할 수 있는 구조가 필요하다는 걸 알게 됐습니다.
배포 후에는 캐시 문제도 많이 겪었습니다. 코드를 수정했는데 실제 사이트에서는 예전 화면이 계속 보이는 경우가 있었습니다. 처음에는 배포가 안 된 줄 알았는데, 알고 보니 브라우저 캐시나 서비스 워커 캐시가 남아 있는 문제였습니다. 그래서 서비스 워커 캐시 버전을 올리고, HTML이나 JS 파일은 너무 오래 캐시되지 않도록 설정을 바꿨습니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/6b3abafe-4082-4a22-a03f-5b0290459cbe/image.png" alt=""></p>
<p>특히 PWA를 적용하면 장점도 있지만, 캐시 관리가 더 중요해졌습니다. 사용자는 앱처럼 빠르게 접근할 수 있지만, 개발자는 업데이트가 제대로 반영되는지 계속 신경 써야 했습니다. 기능을 고쳤는데 사용자가 예전 파일을 보고 있으면 실제로는 버그가 고쳐지지 않은 것처럼 보이기 때문입니다.
이 과정에서 느낀 점은, 앱을 만드는 것과 앱을 공개하는 것은 다른 단계라는 것이었습니다. 예약 기능을 만들고 관리자 페이지를 만드는 건 제품의 핵심 기능을 구현하는 일이었습니다. 하지만 OAuth, 도메인, 검색 색인, 배포 설정, 캐시 처리는 그 제품을 실제 사용자가 접근할 수 있는 서비스로 만드는 일이었습니다.</p>
<p>처음에는 부가 작업이라고 생각했지만, 실제로는 서비스 완성도에 큰 영향을 주는 부분이었습니다. 로그인이 안 되면 사용자는 시작도 못 하고, 도메인이 불안정하면 신뢰가 떨어지고, 검색에 안 잡히면 찾기 어렵고, 캐시가 꼬이면 수정한 기능이 반영되지 않습니다.</p>
<p>이번 프로젝트를 하면서 단순히 기능을 만드는 것뿐만 아니라, 실제 서비스처럼 운영되기 위해 필요한 주변 작업들도 경험해볼 수 있었습니다. OAuth 설정, 도메인 연결, 검색 색인, 배포 환경 차이, 캐시 문제까지 직접 부딪혀보면서 웹앱을 공개하는 과정이 생각보다 넓다는 걸 알게 됐습니다.
다음에는 이 프로젝트를 전체적으로 돌아보면서, 처음 생각했던 것과 달랐던 점, 가장 오래 걸렸던 부분, 그리고 다시 만든다면 다르게 설계하고 싶은 부분들을 정리해보려고 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스터디 카페 앱 제작 #4]]></title>
            <link>https://velog.io/@ihopenre-eng/%EC%8A%A4%ED%84%B0%EB%94%94-%EC%B9%B4%ED%8E%98-%EC%95%B1-%EC%A0%9C%EC%9E%91-4</link>
            <guid>https://velog.io/@ihopenre-eng/%EC%8A%A4%ED%84%B0%EB%94%94-%EC%B9%B4%ED%8E%98-%EC%95%B1-%EC%A0%9C%EC%9E%91-4</guid>
            <pubDate>Wed, 15 Jul 2026 07:11:38 GMT</pubDate>
            <description><![CDATA[<p>3편에서는 관리자 페이지를 만들면서 예약 승인, 좌석 모니터, 회원 관리, 매출 정리 같은 기능을 어떻게 추가했는지 정리해봤습니다. 이번에는 기능을 만든 뒤 실제로 운영할 수 있게 다듬었던 부분들을 적어보려고 합니다.</p>
<p>처음에는 예약 기능과 관리자 페이지가 있으면 어느 정도 완성됐다고 생각했습니다. 사용자가 예약하고, 관리자가 승인하고, 좌석 상태를 확인할 수 있으면 기본 흐름은 돌아가기 때문입니다. 그런데 실제 운영을 생각해보니까 그것만으로는 부족했습니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/5a4d09b3-5693-4720-b09d-9900c968c11b/image.png" alt=""></p>
<p>가장 먼저 신경 쓴 부분은 결제 확인이었습니다. 기존에는 돈 관리도 수기로 하고 있었기 때문에, 온라인 예약을 받으려면 결제 여부를 확인할 수 있어야 했습니다. 처음에는 예약 정보에 결제 방식만 넣어봤습니다. 현장 결제인지, 온라인 결제인지 정도만 구분하면 될 줄 알았습니다.</p>
<p>그런데 온라인 결제는 말로만 “결제했습니다”라고 받으면 관리자가 확인하기 어렵습니다. 그래서 결제 캡처 이미지를 업로드하게 해봤습니다. 사용자가 온라인 결제를 선택하면 결제 증빙을 올리고, 관리자는 관리자 페이지에서 그 이미지를 보고 승인할 수 있게 했습니다.</p>
<p>처음에는 업로드 파일을 서버에 저장하는 방식으로 해봤는데, 배포 환경에서는 로컬 파일 저장이 안정적이지 않을 수 있었습니다. 그래서 나중에는 Cloudinary를 연결해서 업로드 이미지를 외부 저장소에 보관하도록 바꿨습니다. 그리고 아무 URL이나 결제 증빙으로 들어오면 안 되기 때문에, 결제 증빙 URL도 검증하도록 수정했습니다.</p>
<p>알림 기능도 추가해봤습니다. 처음에는 사용자가 예약을 넣고 나면 직접 새로고침해서 상태를 확인해야 했습니다. 그런데 실제 사용자 입장에서는 예약이 승인됐는지, 거절됐는지, 취소됐는지 바로 알 수 있어야 했습니다. 그래서 예약 승인, 거절, 취소, 연장 요청 같은 이벤트가 생기면 알림을 보내는 구조를 만들었습니다.</p>
<p>이 과정에서도 생각보다 고려할 게 많았습니다. 알림 전송이 실패했다고 예약 처리까지 실패하면 안 됐습니다. 예약 승인은 정상적으로 처리하고, 알림은 실패하더라도 전체 기능이 멈추지 않게 해야 했습니다. 그래서 예약 처리 로직과 알림 전송을 분리해서, 알림은 부가적으로 실행되도록 만들었습니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/3b54b907-1c81-437e-a67d-a373e470c267/image.png" alt=""></p>
<p>채팅 기능도 넣어봤습니다. 처음에는 예약 관련 문의가 있으면 따로 연락하면 된다고 생각했는데, 운영을 생각하면 사용자와 관리자가 앱 안에서 바로 문의할 수 있는 게 편했습니다. 그래서 사용자-관리자 채팅을 만들고, 이미지 첨부도 가능하게 했습니다.</p>
<p>처음 채팅 이미지를 처리할 때는 보안 문제가 생길 수 있었습니다. 이미지 URL이나 첨부 데이터를 그대로 화면에 넣으면 XSS 같은 문제가 생길 수 있어서, 클릭 이벤트에 데이터를 직접 넣는 방식을 피하고 안전하게 렌더링하도록 수정했습니다. 단순히 “이미지가 보이면 된다”가 아니라, 사용자가 올리는 데이터는 항상 조심해서 다뤄야 한다는 걸 느꼈습니다.</p>
<p>PWA도 적용해봤습니다. 앱처럼 홈 화면에 추가해서 사용할 수 있으면 직원이나 사용자가 더 편하게 접근할 수 있을 것 같았습니다. 그래서 manifest, 아이콘, 서비스 워커 캐시를 추가했습니다. 처음에는 캐시를 넣으면 성능이 좋아질 줄 알았는데, 실제로는 캐시 때문에 오래된 화면이 남거나 Safari에서 리다이렉션 문제가 생기기도 했습니다.</p>
<p>그래서 서비스 워커 캐시 버전을 계속 올리고, HTML이나 JS처럼 자주 바뀌는 파일은 캐시 정책을 조정했습니다. 특히 관리자 페이지처럼 최신 데이터가 중요한 화면은 캐시가 오히려 문제가 될 수 있어서 더 조심해야 했습니다.</p>
<p>모바일 대응도 오래 걸렸습니다. 실제 사용자는 대부분 모바일로 접속할 가능성이 높았기 때문에, 화면이 PC에서만 잘 보여서는 의미가 없었습니다. 하단 네비게이션, 스크롤, 터치 이벤트, 로그인 버튼, 팝업 위치를 계속 수정했습니다. iOS나 삼성 브라우저에서는 스크롤이 잠기거나 버튼이 잘리는 문제가 있어서, 레이아웃을 여러 번 바꿔봤습니다.</p>
<p>특히 모바일에서는 작은 UI 차이가 사용성을 크게 바꿨습니다. 버튼이 살짝 겹치거나, 팝업이 화면 밖으로 나가거나, 하단 네비게이션이 내용을 가리면 바로 불편해졌습니다. 그래서 모바일에서는 보기 좋은 디자인보다 실제로 눌리고, 스크롤되고, 안 가려지는 구조가 더 중요했습니다.</p>
<p>보안도 중간부터 계속 보강했습니다. 처음에는 기능 구현에 집중했지만, 관리자 페이지와 파일 업로드가 들어가면서 보안 처리가 중요해졌습니다. 관리자 페이지는 프론트에서만 막으면 안 되고, 서버에서도 관리자 권한을 확인해야 했습니다. 그래서 관리자 페이지 접근을 서버에서 다시 검증하도록 바꿨습니다.</p>
<p>또 업로드 파일, 채팅 첨부 이미지, 공지 이미지처럼 사용자가 넣는 데이터는 전부 위험할 수 있다고 보고 검증을 추가했습니다. 요청 횟수 제한, MongoDB 쿼리 sanitizing, CSP, 민감 경로 차단 같은 설정도 넣었습니다. 만들면서 느낀 건, 운영 시스템은 기능이 많아질수록 공격 표면도 같이 넓어진다는 점이었습니다.</p>
<p>결국 4편에서 정리한 작업들은 눈에 확 띄는 핵심 기능은 아닐 수 있습니다. 하지만 실제로 서비스를 운영하려면 꼭 필요한 부분들이었습니다. 결제 증빙이 있어야 관리자가 신뢰할 수 있고, 알림이 있어야 사용자가 상태를 알 수 있고, 채팅이 있어야 문의가 줄어들고, PWA와 모바일 대응이 되어야 실제 환경에서 쓸 수 있고, 보안 처리가 되어야 관리자 기능을 안전하게 열 수 있었습니다.</p>
<p>처음에는 예약 기능만 완성하면 끝이라고 생각했습니다. 그런데 실제로는 그 이후부터가 운영 품질을 만드는 과정이었습니다. 기능이 “된다”에서 끝나는 게 아니라, 사용자가 헷갈리지 않고, 관리자가 실수하지 않고, 모바일에서도 안정적으로 동작하고, 보안적으로도 문제가 없게 계속 다듬는 작업이 더 오래 걸렸습니다.</p>
<p>다음 글에서는 앱 기능 자체보다는 서비스를 실제로 공개하기 위해 했던 부가 작업들을 정리해보려고 합니다. 카카오 로그인과 구글 OAuth를 붙이면서 겪었던 문제, 도메인 연결, Google Search Console 색인, sitemap과 robots 설정, 배포 환경에서 생긴 문제들을 다뤄보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스터디 카페 앱 제작 #3]]></title>
            <link>https://velog.io/@ihopenre-eng/%EC%8A%A4%ED%84%B0%EB%94%94-%EC%B9%B4%ED%8E%98-%EC%95%B1-%EC%A0%9C%EC%9E%91-3</link>
            <guid>https://velog.io/@ihopenre-eng/%EC%8A%A4%ED%84%B0%EB%94%94-%EC%B9%B4%ED%8E%98-%EC%95%B1-%EC%A0%9C%EC%9E%91-3</guid>
            <pubDate>Wed, 15 Jul 2026 07:05:14 GMT</pubDate>
            <description><![CDATA[<p>2편에서는 3층/4층 좌석권과 M층 시간권을 다르게 처리했던 예약 구조에 대해 정리해봤습니다. 이번에는 관리자 페이지를 만들면서 겪었던 부분을 적어보려고 합니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/e3e6f822-c45a-446c-8d95-176a5d5f717a/image.png" alt=""></p>
<p>처음에는 관리자 페이지를 단순하게 생각했습니다. 사용자가 예약을 신청하면 관리자가 그 예약을 보고 승인하거나 거절하면 된다고 생각했습니다. 그래서 처음에는 예약 목록, 승인 버튼, 거절 버튼 정도만 만들어봤습니다.
그런데 실제 운영을 생각해보니까 예약 목록만으로는 부족했습니다. 직원 입장에서는 “예약이 들어왔는지”보다 “지금 어떤 자리가 사용 중인지”, “누가 언제까지 쓰는지”, “결제가 확인됐는지”, “이 예약이 매출에 들어가도 되는지”를 바로 확인하는 게 더 중요했습니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/6558e6ea-4b76-4478-ad8d-a576f2e96a9d/image.png" alt=""></p>
<p>그래서 먼저 실시간 좌석 모니터를 만들어봤습니다. 좌석마다 비어 있음, 사용 중, 승인 대기 상태를 표시하게 했습니다. 기존에는 직원이 직접 층을 올라가서 자리를 확인해야 했는데, 이제는 관리자 화면에서 바로 볼 수 있게 만들고 싶었습니다.</p>
<p>처음에는 좌석 상태만 표시하면 될 줄 알았습니다. 그런데 막상 써보니까 좌석을 클릭했을 때 그 자리를 누가 쓰고 있는지, 언제까지 이용하는지, 어떤 이용권인지까지 보여줘야 했습니다. 그래서 좌석 클릭 시 예약자 정보와 이용 기간을 확인할 수 있게 수정했습니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/b24dec83-5e5c-4df5-bd61-d4f61f90ad21/image.png" alt=""></p>
<p>회원 관리도 추가해봤습니다. 처음에는 사용자가 직접 가입하고 예약하면 된다고 생각했는데, 실제 운영에서는 관리자가 직접 회원을 등록하거나 정보를 수정해야 하는 경우도 있었습니다. 그래서 관리자 페이지에서 회원 등록, 비밀번호 변경, 전화번호 수정, 회원별 예약 조회 기능을 넣었습니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/34aaf310-3242-40a7-9351-ecbd13ee514a/image.png" alt=""></p>
<p>이용권 관리도 생각보다 중요했습니다. 예약을 잘못 넣었거나 현장에서 이용권을 바꾸는 경우가 있을 수 있어서, 관리자가 1일권, 7일권, 30일권 같은 이용 기간을 수정할 수 있게 했습니다. 처음에는 단순히 날짜만 바꾸면 된다고 생각했는데, 기간이 바뀌면 다른 예약과 겹칠 수도 있어서 다시 중복 검사를 넣어야 했습니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/be5c9242-10db-4f02-b06b-b2270268d82b/image.png" alt=""></p>
<p>매출 관리도 수기 업무를 줄이기 위해 넣었습니다. 기존에는 돈 관리도 따로 수기로 정리하고 있었기 때문에, 예약 데이터를 기준으로 매출을 계산해보려고 했습니다. 그런데 여기서도 문제가 있었습니다. 예약 데이터가 있다고 해서 전부 매출로 보면 안 됐습니다. 취소된 예약, 거절된 예약, 테스트 예약까지 합산되면 실제 매출과 맞지 않았습니다.</p>
<p>그래서 예약 상태별로 매출 반영 여부를 나누고, 테스트 예약은 매출에서 제외할 수 있게 만들었습니다. 처음에는 단순히 전체 예약 금액을 더하는 방식으로 해봤는데, 실제 운영 데이터로 쓰려면 예외 처리가 훨씬 중요하다는 걸 느꼈습니다.</p>
<p>환불 처리도 나중에 추가했습니다. 이용자가 중간에 취소하면 사용한 날짜, 이용권 종류, 무료 음료 사용 여부 같은 걸 고려해야 했습니다. 처음에는 환불을 수기로 계산해도 되지 않을까 생각했는데, 관리자 페이지에서 바로 계산하고 기록할 수 있어야 나중에 헷갈리지 않을 것 같았습니다. 그래서 환불 금액과 사유를 남길 수 있게 정리했습니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/0e278661-4782-4963-8994-9edc5aa64c09/image.png" alt=""></p>
<p>관리자 권한도 분리해봤습니다. 처음에는 관리자 계정 하나로 모든 기능을 쓰게 했는데, 실제로는 직원이 할 수 있는 일과 사장님만 해야 하는 일이 달랐습니다. 예를 들어 예약 확인이나 좌석 관리는 직원도 할 수 있지만, 민감한 초기화나 중요한 운영 기록 삭제는 사장님 권한으로 제한하는 게 맞다고 생각했습니다. 그래서 admin과 owner 권한을 나눴습니다.</p>
<p>만들면서 느낀 점은 관리자 페이지는 단순히 기능을 많이 넣는다고 좋은 게 아니라는 점이었습니다. 운영자가 실수하지 않게 만드는 게 더 중요했습니다. 잘못된 예약을 승인하지 않게 하고, 매출이 중복으로 잡히지 않게 하고, 좌석 상태가 실제 운영과 다르게 보이지 않게 계속 수정해야 했습니다.</p>
<p>처음에는 예약 승인용 화면으로 시작했지만, 만들다 보니 관리자 페이지는 카페 운영의 중심이 됐습니다. 좌석 확인, 예약 승인, 회원 관리, 이용권 수정, 매출 정리, 환불 처리까지 기존에 수기로 하던 업무를 한 화면 안으로 옮기는 작업이었습니다.</p>
<p>다음 글에서는 운영 품질을 높이기 위해 추가했던 기능들을 정리해보려고 합니다. 결제 증빙 업로드, 알림, 채팅, PWA, 모바일 브라우저 대응, 보안 패치 같은 부분을 다뤄보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스터디 카페 앱 제작 #2]]></title>
            <link>https://velog.io/@ihopenre-eng/%EC%8A%A4%ED%84%B0%EB%94%94-%EC%B9%B4%ED%8E%98-%EC%95%B1-%EC%A0%9C%EC%9E%91-2</link>
            <guid>https://velog.io/@ihopenre-eng/%EC%8A%A4%ED%84%B0%EB%94%94-%EC%B9%B4%ED%8E%98-%EC%95%B1-%EC%A0%9C%EC%9E%91-2</guid>
            <pubDate>Wed, 15 Jul 2026 06:55:16 GMT</pubDate>
            <description><![CDATA[<p>1편에서는 왜 카페 운영 시스템을 만들게 되었는지 정리해봤습니다. 이번에는 이 앱에서 가장 중요했던 예약 구조에 대해 적어보려고 합니다.</p>
<p>처음에는 예약 기능을 단순하게 생각했습니다. 사용자가 자리를 고르고, 날짜를 선택하고, 예약을 신청하면 끝이라고 생각했습니다. 그래서 처음에는 모든 예약을 거의 같은 방식으로 처리해보려고 했습니다. 좌석 번호, 날짜, 이용 기간 정도만 있으면 될 것 같았습니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/0b345101-2e8c-4165-a0e6-31cab9eccc3f/image.png" alt=""></p>
<p>그런데 실제로 만들어보니까 3층/4층 예약과 M층 예약은 완전히 다른 방식으로 봐야 했습니다.</p>
<p>3층과 4층은 좌석권 중심이었습니다. 예를 들어 1일권, 7일권, 30일권처럼 특정 좌석을 며칠 동안 사용하는 구조였습니다. 그래서 중요한 값은 “어떤 좌석을”, “언제부터”, “며칠 동안” 사용하는지였습니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/e3e6b8c9-0eb6-4452-b445-52377694b8af/image.png" alt=""></p>
<p>반면 M층은 시간권 중심이었습니다. 좌석 하나를 며칠 동안 잡아두는 방식이 아니라, 특정 날짜의 특정 시간대에 공간을 예약하는 방식이었습니다. 그래서 “몇 월 며칠”, “몇 시부터”, “몇 시까지”가 중요했습니다. 이걸 3층/4층 좌석권과 똑같이 처리하려고 하니까 바로 문제가 생겼습니다.</p>
<p>처음에는 M층도 그냥 날짜와 이용 시간만 저장하면 되겠다고 생각했습니다. 그런데 그렇게 해보니까 같은 시간대에 예약이 겹치는 문제가 생길 수 있었습니다. 예를 들어 누군가 오후 2시부터 4시까지 예약했는데, 다른 사용자가 오후 3시부터 5시까지 예약하면 둘 다 같은 시간대를 쓰게 됩니다. 단순히 날짜만 비교하면 이런 충돌을 막을 수 없었습니다.</p>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/b0da1f61-5f8d-476e-8c4d-85d869bb9cd2/image.png" alt=""></p>
<p>그래서 M층 예약은 시작 시간과 종료 시간을 따로 저장하고, 새 예약이 들어올 때 기존 예약들과 시간이 겹치는지 검사하도록 바꿨습니다. 기준은 단순했습니다. 새 예약의 시작 시간이 기존 예약의 종료 시간보다 빠르고, 새 예약의 종료 시간이 기존 예약의 시작 시간보다 늦으면 겹치는 예약으로 보는 방식입니다. 이렇게 바꾸니까 같은 날짜 안에서 시간대가 겹치는 예약을 막을 수 있었습니다.</p>
<p>또 하나 막혔던 부분은 이용 시간 단위였습니다. 처음에는 1시간 단위로만 예약하면 충분할 줄 알았습니다. 그런데 실제 운영에서는 1시간 30분 같은 예약도 필요할 수 있었습니다. 그래서 M층은 30분 단위 예약을 지원하도록 바꿨습니다. 시작 시간도 30분 단위로 선택할 수 있게 하고, 이용 시간도 1시간, 1시간 30분처럼 처리할 수 있게 수정했습니다.</p>
<p>이 과정에서 생각보다 자잘한 버그도 많이 나왔습니다. 예를 들어 M층은 일반 좌석처럼 좌석 번호가 명확하지 않아서 내부적으로 0 같은 값을 쓰는 경우가 있었는데, 처음 유효성 검사에서는 0을 잘못된 값처럼 처리하는 문제가 있었습니다. JavaScript에서 0은 falsy 값이라 단순 조건문으로 검사하면 없는 값처럼 판단될 수 있었습니다. 그래서 좌석 번호가 있는지 없는지를 대충 검사하지 않고, 숫자인지와 허용 범위 안에 있는지를 명확하게 검사하도록 바꿨습니다.</p>
<p>예약 상태도 처음보다 더 세분화했습니다. 처음에는 예약이 있냐 없냐 정도만 생각했는데, 실제 운영에서는 그걸로 부족했습니다. 사용자가 예약을 신청했지만 아직 관리자가 확인하지 않은 상태, 관리자가 승인한 상태, 거절된 상태, 사용자가 취소한 상태, 이용이 끝난 상태가 모두 달랐습니다. 그래서 예약 상태를 대기, 승인, 거절, 취소, 완료처럼 나눴습니다.</p>
<p>이렇게 상태를 나누니까 관리자 페이지도 훨씬 명확해졌습니다. 관리자는 어떤 예약을 승인해야 하는지, 어떤 좌석이 실제 사용 중인지, 어떤 예약은 매출에 반영해도 되는지를 구분할 수 있었습니다. 특히 매출 계산에서는 단순히 예약 데이터가 있다고 전부 더하면 안 됐습니다. 거절된 예약이나 취소된 예약, 테스트 예약까지 매출에 들어가면 실제 운영 데이터가 틀어지기 때문입니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/1b8a2048-6259-459d-96ef-845520f18b40/image.png" alt=""></p>
<p>반복 예약도 쉽지 않았습니다. M층은 한 번만 예약하는 경우도 있지만, 매주 같은 요일에 반복해서 예약하는 경우도 필요했습니다. 처음에는 반복 예약을 단순히 여러 개의 예약으로 생성하면 된다고 생각했습니다. 그런데 실제로는 각 날짜마다 기존 예약과 시간이 겹치는지 모두 확인해야 했습니다. 하나라도 겹치면 전체 예약을 막거나, 사용자가 다시 선택할 수 있게 해야 했습니다. 그래서 반복 예약도 결국 “여러 개를 한 번에 만드는 기능”이 아니라, 각각의 날짜와 시간대에 대해 충돌 검사를 하는 구조로 봐야 했습니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/d5e10e0f-dfac-4103-b17a-c641e212afe2/image.png" alt=""></p>
<p>결국 예약 기능을 만들면서 가장 크게 느낀 점은, 같은 “예약”이라는 단어를 쓰더라도 운영 방식에 따라 데이터 구조가 달라져야 한다는 것이었습니다. 3층/4층은 좌석과 기간이 핵심이고, M층은 날짜와 시간이 핵심이었습니다. 이 차이를 제대로 분리하지 않으면 화면은 그럴듯하게 보여도 실제 운영에서는 바로 문제가 생겼습니다.</p>
<p>처음에는 단순히 예약 폼을 만드는 작업이라고 생각했지만, 실제로는 카페 운영 정책을 데이터 구조로 옮기는 작업이었습니다. 어떤 예약을 허용할지, 어떤 예약을 막을지, 어떤 상태를 관리자에게 보여줄지 하나씩 정리하면서 시스템이 점점 실제 운영에 맞게 바뀌었습니다.</p>
<p>다음 글에서는 관리자 페이지를 어떻게 만들었는지 정리해보려고 합니다. 예약 목록만 보여주는 화면에서 시작해서, 좌석 모니터, 회원 관리, 매출 정리, 환불 처리까지 어떻게 확장됐는지를 다뤄보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스터디 카페 앱 제작 #1]]></title>
            <link>https://velog.io/@ihopenre-eng/%EC%8A%A4%ED%84%B0%EB%94%94-%EC%B9%B4%ED%8E%98-%EC%95%B1-%EC%A0%9C%EC%9E%91-1</link>
            <guid>https://velog.io/@ihopenre-eng/%EC%8A%A4%ED%84%B0%EB%94%94-%EC%B9%B4%ED%8E%98-%EC%95%B1-%EC%A0%9C%EC%9E%91-1</guid>
            <pubDate>Wed, 15 Jul 2026 06:47:07 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/6bac50d9-351b-4a1b-8552-27939f686c1a/image.png" alt=""></p>
<p>이 개발을 시작하게 된 계기는 학원 선생님께서 만들어보면 어떻겠냐고 말씀해주신 것이었습니다. 처음에는 그냥 카페 좌석 예약 페이지 정도를 만들어보면 되겠다고 생각했습니다. 사용자가 자리를 고르고 예약하면 관리자가 확인하는 정도의 간단한 시스템을 떠올렸습니다.</p>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/f80df32b-4ec9-43e0-b264-133d8816485d/image.png" alt=""></p>
<p>그런데 실제 운영 방식을 보니까 생각보다 불편한 부분이 많았습니다. 직원분들이 매번 직접 층을 올라가서 어떤 자리가 비어 있는지 하나씩 확인해야 했고, 이용권도 1일권, 7일권, 30일권처럼 종류별로 따로 수기로 기록하고 있었습니다. 돈 관리도 따로 수기로 정리하고 있었고, 예약 상태나 결제 여부를 한눈에 볼 수 있는 화면도 없었습니다.</p>
<p>그래서 단순한 예약 페이지가 아니라, 카페 운영에서 수기로 하던 일을 온라인으로 정리하는 시스템을 만들어보기로 했습니다.
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/200c87e3-475f-4f7f-9a44-2f7f8724fceb/image.png" alt=""></p>
<p>먼저 좌석을 온라인에서 확인할 수 있게 만들어봤습니다. 처음에는 “빈 자리인지, 사용 중인지 정도만 표시하면 되겠지”라고 생각했는데, 막상 해보니까 그것만으로는 부족했습니다. 예약 신청만 들어온 상태인지, 관리자가 승인한 예약인지, 취소된 예약인지, 이용 기간이 끝난 예약인지가 다 구분되어야 했습니다. 그래서 예약 상태를 대기, 승인, 거절, 취소, 완료처럼 나누고, 관리자 화면에서 바로 확인할 수 있게 정리했습니다.</p>
<p>이용권 관리도 생각보다 까다로웠습니다. 3층과 4층은 좌석을 며칠 동안 쓰는 방식이라 1일권, 7일권, 30일권처럼 기간 중심으로 처리하면 됐습니다. 그런데 M층은 시간 단위로 예약하는 공간이라 날짜뿐만 아니라 시작 시간과 이용 시간까지 계산해야 했습니다. 처음에는 단순하게 처리해봤는데, 같은 시간대에 중복 예약이 생길 수 있어서 시간 충돌을 따로 검사하도록 수정했습니다.</p>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/0f37ff3e-0297-4686-9f0e-3c4f18ca548f/image.png" alt=""></p>
<p>관리자 페이지도 계속 바꿔봤습니다. 처음에는 예약 목록만 보여주면 될 줄 알았는데, 실제로는 관리자가 보고 싶은 정보가 훨씬 많았습니다. 지금 어떤 좌석이 사용 중인지, 누가 언제까지 이용하는지, 결제가 확인됐는지, 매출에 반영해도 되는 예약인지가 중요했습니다. 그래서 좌석 모니터, 예약 승인, 회원 관리, 매출 정리, 환불 처리 같은 기능을 하나씩 추가했습니다.</p>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/fac259bb-25a9-41c4-8ba9-8863fd90fc18/image.png" alt=""></p>
<p>만들면서 가장 크게 느낀 점은, 실제 운영 시스템은 기능 하나하나보다 예외 처리가 더 중요하다는 것이었습니다. 예약이 겹치면 안 되고, 결제 증빙이 있어야 하고, 관리자가 실수로 잘못 승인하지 않게 해야 하고, 모바일에서도 제대로 동작해야 했습니다. 처음에는 간단한 예약 앱으로 시작했지만, 결국 카페에서 수기로 하던 운영 흐름을 온라인으로 옮기는 프로젝트가 되었습니다.</p>
<p>다음 글에서는 이 시스템에서 가장 중요했던 예약 구조를 정리해보려고 합니다. 특히 3층/4층 좌석권과 M층 시간권을 왜 다르게 설계했는지, 그리고 중복 예약 문제를 어떻게 해결했는지를 다뤄보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[피카츄 배구 메모리 값 수정으로 간단한 해킹]]></title>
            <link>https://velog.io/@ihopenre-eng/%ED%94%BC%EC%B9%B4%EC%B8%84-%EB%B0%B0%EA%B5%AC-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EA%B0%92-%EC%88%98%EC%A0%95%EC%9C%BC%EB%A1%9C-%EA%B0%84%EB%8B%A8%ED%95%9C-%ED%95%B4%ED%82%B9</link>
            <guid>https://velog.io/@ihopenre-eng/%ED%94%BC%EC%B9%B4%EC%B8%84-%EB%B0%B0%EA%B5%AC-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EA%B0%92-%EC%88%98%EC%A0%95%EC%9C%BC%EB%A1%9C-%EA%B0%84%EB%8B%A8%ED%95%9C-%ED%95%B4%ED%82%B9</guid>
            <pubDate>Wed, 15 Jul 2026 06:33:44 GMT</pubDate>
            <description><![CDATA[<h2 id="실습-환경">실습 환경</h2>
<p>Windows
피카츄 배구
Cheat Engine</p>
<p>메모리 값 찾기</p>
<p>먼저 게임을 실행한 후 Cheat Engine으로 프로세스에 연결합니다.</p>
<p>게임에서 현재 점수를 확인한 뒤 해당 숫자를 검색합니다.</p>
<p>예를 들어 점수가 3점이라면 3을 검색하고, 이후 점수가 변경될 때마다 다시 검색(Next Scan)을 반복하면 후보 주소가 점점 줄어듭니다.</p>
<p>최종적으로 실제 점수가 저장되는 메모리 주소를 찾을 수 있습니다.</p>
<h2 id="메모리-값-수정">메모리 값 수정</h2>
<p>점수가 저장된 주소를 찾은 후 값을 원하는 숫자로 변경했습니다.</p>
<p>예를 들어</p>
<p>3 → 99
5 → 100</p>
<p>처럼 수정하면 게임 내 점수가 즉시 변경되는 것을 확인할 수 있습니다.</p>
<p>이는 게임이 화면에 표시하는 값이 메모리에 저장된 데이터를 그대로 사용하기 때문입니다.</p>
<p>실습 결과</p>
<details>
<summary>동영상 보기 (클릭)</summary>
<iframe width="100%" height="400" src="https://www.youtube.com/embed/IEz3-VmQteg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</details>

<p>영상처럼 상대 피카츄가 득점을 해도 메모리 값이 7로 고정되어있기 때문에 7에서 늘어나지 않는 모습을 볼수있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Hak5 usb 를 이용하여 자동 인증서 설치 및 서버 접속을 통하여 실시간 원격 트래픽 분석하기(mitmproxy/Rubber Ducky)]]></title>
            <link>https://velog.io/@ihopenre-eng/Hak5-usb-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%EC%9E%90%EB%8F%99-%EC%9D%B8%EC%A6%9D%EC%84%9C-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%84%9C%EB%B2%84-%EC%A0%91%EC%86%8D%EC%9D%84-%ED%86%B5%ED%95%98%EC%97%AC-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%9B%90%EA%B2%A9-%ED%8A%B8%EB%9E%98%ED%94%BD-%EB%B6%84%EC%84%9D%ED%95%98%EA%B8%B0mitmproxyRubber-Ducky</link>
            <guid>https://velog.io/@ihopenre-eng/Hak5-usb-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%EC%9E%90%EB%8F%99-%EC%9D%B8%EC%A6%9D%EC%84%9C-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%84%9C%EB%B2%84-%EC%A0%91%EC%86%8D%EC%9D%84-%ED%86%B5%ED%95%98%EC%97%AC-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%9B%90%EA%B2%A9-%ED%8A%B8%EB%9E%98%ED%94%BD-%EB%B6%84%EC%84%9D%ED%95%98%EA%B8%B0mitmproxyRubber-Ducky</guid>
            <pubDate>Wed, 15 Jul 2026 05:36:31 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/3d0cecf9-a779-4a37-bb8f-e6ff52125e51/image.png" alt=""></p>
<p>일단 이 bad usb 라고 불리는 Hak5 Rubber Ducky라는 기기에 대해서 간단하게 설명해보자면 영화에서 많이 나온 usb를 꼽자마자 자동으로 막 해킹이 되는 장면이 많은데 실제로 그 공격을 수행할수있도록 해주는  기기 입니다. </p>
<p>이 usb에는 payload를 미리 저장하여 자동으로 실행시킬수 있습니다.</p>
<p>이제 트래픽 분석에 대해서 알아보겠습니다.</p>
<p>먼저 공격자 컴퓨터에서 서버를 아래처럼 열고 (8000포트로 수행했습니다)
<img src="https://velog.velcdn.com/images/ihopenre-eng/post/c05e011e-036c-44cf-9ddb-1594961f5964/image.png" alt="">
이제  mitmproxy --listen-port 8000 --showhost 라는 명령어를 통해서 8000포트를 사용하여 통신하는 모든 패킷들을 볼수있게 세팅 해줍니다.<img src="https://velog.velcdn.com/images/ihopenre-eng/post/6221a879-8c9a-4901-807c-9a6a9127dab5/image.png" alt="">
또한 공격을 하기 전에 미리 해야할것이 있는데 바로 인증서를 서버에 업로드 하는 것 입니다. 저는 mitmproxy 트래픽을 가로채기 위한 프록시 툴을 사용하여 공격을 수행하였습니다.</p>
<p>mitmproxy를 사용하여 인증서를 생성하고 상대 컴퓨터에서는 usb가</p>
<pre><code>cd $env:USERPROFILE\.mitmproxy
python -m http.server 8000</code></pre><p>라는 코드를 실행시켜</p>
<p>아까 생성한 8000포트 서버를 통하여 인증서를 불러오게 시킵니다.</p>
<p>그러면 상대방은 아까 생성한 프록시 서버에 접속하게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/103e4dfe-43cf-4b37-ab12-8cb013fd8a3a/image.png" alt="">
이렇게 상대방이 어떤 패킷을 주고 받는지를 볼수있게 됩니다.</p>
<pre><code>(세션 쿠키,아이디/비밀번호,다운로드중인 파일 내용,현재 입력중인 것들을 실시간으로 확인 가능)</code></pre><p>하지만 이 공격은 상대방과 동일한 네트워크에 접속한 상태라는 가정하에 수행가능 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Subway Surfers 무한 점프 만들기 (Cheat Engine/LD player)]]></title>
            <link>https://velog.io/@ihopenre-eng/Subway-Surfers-%EB%AC%B4%ED%95%9C-%EC%A0%90%ED%94%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-Cheat-EngineLD-player</link>
            <guid>https://velog.io/@ihopenre-eng/Subway-Surfers-%EB%AC%B4%ED%95%9C-%EC%A0%90%ED%94%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-Cheat-EngineLD-player</guid>
            <pubDate>Wed, 15 Jul 2026 03:28:05 GMT</pubDate>
            <description><![CDATA[<p>오늘은 Cheat Engine 을 사용하여 간단한 메모리 조작을 통해서 Subway Surfers 라는 게임을 조작해 보았습니다.</p>
<p> <img src="https://velog.velcdn.com/images/ihopenre-eng/post/09f17bc2-756b-4613-8c2d-cba69473b3b1/image.png" alt=""></p>
<p>일단 Unknown initial value 로 scan을 시작합니다. 왜냐하면 게임 내 어떤 변수(예: 체력, 스태미나, 스코어 등)의 정확한 값을 모르기 때문에 사용합니다  예를 들어 체력바가 숫자가 아닌 그래픽으로 표시된다면 값이 몇인지 알 수 없기 때문에 &quot;Unknown initial value&quot;로 시작합니다. 그리고 이 게임은 점프할 때마다 내부 값이 변하기 때문에 점프 전후로 스캔을 반복하면서 그 차이를 통해 관련 값들을 찾아 내야합니다.</p>
<p><img src="https://velog.velcdn.com/images/ihopenre-eng/post/99e45e48-c7a1-4dfc-8ba5-01c9b5e319b8/image.png" alt=""></p>
<p>위에 사진은 게임내에서 캐릭터가 점프할때와 착지 했을때를 changed value로 scan 하여서</p>
<p>점프할수있을때가 1 할수없을때값 0을 측정하여 고정한 것 입니다.</p>
<details>
  <summary>Subway Surfers 무한 점프 영상</summary>
  <br />
  <iframe width="100%" height="500" src="https://www.youtube.com/embed/QzdpxnB_ao0" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</details>



<p>이런식으로 값들을 고정하여 계속해서 점프할수있는 상태를 유지시켜 무한 점프가 가능하도록 메모리를 조작하였습니다.</p>
]]></description>
        </item>
    </channel>
</rss>