๊ทธ๋๋ณด๋7 ๋ ์ด์์ JSON ๋ณด์ ์ํคํ ์ฒ โ Custom Validation Rules, FormRequest ๊ฒ์ฆ ๊ณ์ธต, ๊ณต๊ฒฉ ๋ฐฉ์ด ์ ๋ต
1. ๋ค์ธต ๋ฐฉ์ด: FormRequest(10๊ฐ Custom Rule) โ Service โ Repository(ORM) โ React(์๋ ์ด์ค์ผ์ดํ)
2. 10๊ฐ Custom Rule: JSON ๊ตฌ์กฐ, ์ปดํฌ๋ํธ, ์๋ํฌ์ธํธ, URL, ์์, ์ฌ๋กฏ, ๋ฐ์ดํฐ์์ค, ๊ถํ, ๊ฒฝ๋ก, ํ์ผํ์
3. 7๊ฐ FormRequest: ์ฉ๋๋ณ Rule ์กฐํฉ ์ฐจ๋ฑ ์ ์ฉ (Store/Update/Content/Inheritance/Preview/Get)
4. ์์ ๋ณด์: ์ํ ์ฐธ์กฐ ๊ฐ์ง + ๊น์ด 10 ์ ํ + ๋ถ๋ชจ ์ฌ๋กฏ/๋ฐ์ดํฐ์์ค ID ๊ณ ์ ์ฑ ๊ฒ์ฆ
5. ํ์ฅ ๊ฐ๋ฅ: 6๊ฐ Hook์ผ๋ก ๋ชจ๋/ํ๋ฌ๊ทธ์ธ์ด ๊ฒ์ฆ ๊ท์น์ ๋์ ์ผ๋ก ์ถ๊ฐ ๊ฐ๋ฅ
- ๊ฐ์
- ๋ณด์ ์ํคํ ์ฒ
- ๊ฒ์ฆ ๊ณ์ธต
- Custom Validation Rules
- FormRequest๋ณ Rule ์ ์ฉ ํํฉ
- ๋ ์ด์์ ์์ ๋ณด์
- ๊ฒฝ๋ก ๋ณด์ (Path Traversal ๋ฐฉ์ด)
- Hook ๊ธฐ๋ฐ ๊ฒ์ฆ ํ์ฅ
- ๊ณต๊ฒฉ ๋ฐฉ์ด ์ ๋ต
- ๋ณด์ ๋ชจ๋ฒ ์ฌ๋ก
- ๊ด๋ จ ๋ฌธ์
๊ทธ๋๋ณด๋7 ํ ํ๋ฆฟ ์์ง์ JSON ๊ธฐ๋ฐ ๋ ์ด์์์ผ๋ก ํ๋ฉด์ ๋์ ์ผ๋ก ๊ตฌ์ฑํฉ๋๋ค. ์ด ์ ์ฐ์ฑ์ ์ ์์ ์ ๋ ฅ์ ํตํ ๊ณต๊ฒฉ ๊ฐ๋ฅ์ฑ์ ๋ดํฌํ๋ฏ๋ก, 10๊ฐ Custom Validation Rule๊ณผ 7๊ฐ FormRequest๋ฅผ ์กฐํฉํ ๋ค์ธต ๋ฐฉ์ด ์ฒด๊ณ๋ก ๋ณดํธํฉ๋๋ค.
- ๋ค์ธต ๋ฐฉ์ด (Defense in Depth): FormRequest โ Service โ Repository โ ํ๋ก ํธ์๋, ๊ฐ ๊ณ์ธต์์ ๋ ๋ฆฝ์ ๊ฒ์ฆ
- ์ต์ ๊ถํ ์์น (Principle of Least Privilege): ํ์ํ ์ต์ํ์ ๊ถํ๋ง ๋ถ์ฌ
- ํ์ดํธ๋ฆฌ์คํธ ๋ฐฉ์ (Whitelist Approach): ๋ช ์์ ์ผ๋ก ํ์ฉ๋ ๊ฒ๋ง ํต๊ณผ
- ๊ฒ์ฆ ์ฐ์ (Fail Secure): ๊ฒ์ฆ ์คํจ ์ ์์ ํ ์ํ๋ก ๋ณต๊ท
- ํ์ฅ ๊ฐ๋ฅํ ๊ฒ์ฆ (Extensible Validation): Hook์ ํตํด ๋ชจ๋/ํ๋ฌ๊ทธ์ธ์ด ๊ฒ์ฆ ๊ท์น์ ๋์ ์ผ๋ก ์ถ๊ฐ ๊ฐ๋ฅ
์ฌ์ฉ์ ์์ฒญ
โ
1. Controller ์ง์
์ ๊ฒ์ฆ (FormRequest + 10๊ฐ Custom Rule)
โโ ValidLayoutStructure: JSON ์คํค๋ง + ์ค์ฒฉ ๊น์ด + actions + permissions ๊ฒ์ฆ
โโ ComponentExists: ์ปดํฌ๋ํธ ๋งค๋ํ์คํธ ๋์กฐ (3์นดํ
๊ณ ๋ฆฌ, 1์๊ฐ ์บ์ฑ)
โโ WhitelistedEndpoint: API ์๋ํฌ์ธํธ ํ์ดํธ๋ฆฌ์คํธ + ๊ฒฝ๋ก ํธ๋๋ฒ์ค ์ฐจ๋จ
โโ NoExternalUrls: 7๊ฐ ์ํ URI ์คํด + ํ๋กํ ์ฝ ์๋ URL ์ฐจ๋จ
โโ ValidParentLayout: ์์ ์ํ ์ฐธ์กฐ ๊ฐ์ง + ๊น์ด 10 ์ ํ
โโ ValidSlotStructure: ๋ถ๋ชจ์์ ์ ์๋ ์ฌ๋กฏ๋ง ํ์ฉ
โโ ValidDataSourceMerge: ์์ ์ฒด์ธ ์ ์ฒด ๋ฐ์ดํฐ์์ค ID ๊ณ ์ ์ฑ
โโ ValidPermissionStructure: or/and ๊ตฌ์กฐ + ๊น์ด 3 ์ ํ + ์ ๊ท์ ํ์
โโ SafeTemplatePath: 13๊ฐ Path Traversal ํจํด + 5ํ URL ๋์ฝ๋ฉ + NULL ๋ฐ์ดํธ
โโ AllowedTemplateFileType: 14๊ฐ ํ์ฅ์ ํ์ดํธ๋ฆฌ์คํธ
โ
2. Hook ๊ธฐ๋ฐ ํ์ฅ ๊ฒ์ฆ (๋ชจ๋/ํ๋ฌ๊ทธ์ธ์ด ์ถ๊ฐํ ๊ท์น)
โ
3. Service ๊ณ์ธต (๋น์ฆ๋์ค ๋ก์ง)
โโ ๋ ์ด์์ ์์ ๋ณํฉ
โโ ํ
์คํ (before_save, after_save)
โโ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ์ฅ
โ
4. ํ๋ก ํธ์๋ ๋ ๋๋ง
โโ React ์๋ ์ด์ค์ผ์ดํ (XSS ๋ฐฉ์ง)
โโ CSRF ํ ํฐ ๊ฒ์ฆ (Laravel Sanctum Bearer ํ ํฐ)
โโ Eloquent ORM ํ๋ผ๋ฏธํฐ ๋ฐ์ธ๋ฉ (SQL Injection ๋ฐฉ์ง)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ์ฌ์ฉ์ ๋ธ๋ผ์ฐ์ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ React ๋ ๋๋ง (์๋ ์ด์ค์ผ์ดํ) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ HTTPS
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Laravel Backend โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ 1. FormRequest ๊ฒ์ฆ (Controller ์ง์
์ ) โ โ
โ โ 10๊ฐ Custom Rule + Hook ํ์ฅ ๊ท์น โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ 2. Service ๊ณ์ธต (๋น์ฆ๋์ค ๋ก์ง) โ โ
โ โ ๋ ์ด์์ ์์ ๋ณํฉ + ํ
์คํ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ 3. Repository ๊ณ์ธต (๋ฐ์ดํฐ ์ ๊ทผ) โ โ
โ โ Eloquent ORM (ํ๋ผ๋ฏธํฐ ๋ฐ์ธ๋ฉ) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MySQL Database โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
์์น: app/Http/Requests/Layout/
๋ชฉ์ : Controller์ ๋๋ฌํ๊ธฐ ์ ์ ๋ชจ๋ ์ ์์ ์ ๋ ฅ์ ์ฐจ๋จ
G7์ ์ฉ๋๋ณ 7๊ฐ FormRequest๋ก ๊ฒ์ฆ์ ๋ถ๋ฆฌํฉ๋๋ค. ๊ฐ FormRequest๋ ์ฉ๋์ ๋ง๋ Custom Rule ์กฐํฉ์ ์ ์ฉํ๋ฉฐ, Hook์ ํตํด ๋ชจ๋/ํ๋ฌ๊ทธ์ธ์ด ๊ท์น์ ํ์ฅํ ์ ์์ต๋๋ค.
์์น: app/Services/LayoutService.php
ํน์ง:
- ๊ฒ์ฆ ๋ก์ง ์์ (FormRequest์์ ์๋ฃ)
- ์์ ๋น์ฆ๋์ค ๋ก์ง๋ง ์ฒ๋ฆฌ
- ํ
์์คํ
์ ํตํ ํ์ฅ์ฑ (
before_save,after_save)
์์น: app/Repositories/LayoutRepository.php
ํน์ง:
- Eloquent ORM ์ฌ์ฉ (ํ๋ผ๋ฏธํฐ ๋ฐ์ธ๋ฉ์ผ๋ก SQL Injection ์๋ ๋ฐฉ์ด)
- ์ง์ SQL ๋ฌธ์์ด ์ฐ๊ฒฐ ๊ธ์ง
G7์ ๋ ์ด์์ JSON ๋ณด์์ ์ํด 10๊ฐ์ Custom Validation Rule์ ์ ๊ณตํฉ๋๋ค.
ํ์ผ: app/Rules/ValidLayoutStructure.php
๋ชฉ์ : ๋ ์ด์์ JSON์ ๊ตฌ์กฐ์ ์ ํจ์ฑ ๊ฒ์ฆ
๊ฒ์ฆ ํญ๋ชฉ:
- ํ์ ํ๋ ์กด์ฌ ํ์ธ (
version,layout_name,components) extends๋ ์ด์์:components๋๋slots์ค ํ๋ ํ์- ์ฌ๊ท์ ์ปดํฌ๋ํธ ๊ตฌ์กฐ ๊ฒ์ฆ (type, name ํ์)
- ์ปดํฌ๋ํธ
type:basic,composite,layout์ค ํ๋๋ง ํ์ฉ - ์ต๋ ์ค์ฒฉ ๊น์ด ์ ํ: 10๋จ๊ณ (
MAX_DEPTH = 10) actions๋ฐฐ์ด ๊ตฌ์กฐ ๊ฒ์ฆ (type๋๋event์ค ํ๋ ํ์)permissionsํ๋:ValidPermissionStructureRule์ ์์- ์ฌ๋กฏ ์ฐธ์กฐ(
{ "slot": "name" })์ Partial ์ฐธ์กฐ({ "partial": "path" }) ํ์ฉ
ํ์ผ: app/Rules/ComponentExists.php
๋ชฉ์ : ์กด์ฌํ์ง ์๋ ์ปดํฌ๋ํธ ์ฐธ์กฐ ๋ฐฉ์ง
๊ฒ์ฆ ๋ฐฉ๋ฒ:
- ํ
ํ๋ฆฟ์
components.json๋งค๋ํ์คํธ ๋ก๋ - 3๊ฐ ์นดํ
๊ณ ๋ฆฌ:
basic,composite,layout์ปดํฌ๋ํธ๋ฅผ Set์ผ๋ก ๊ตฌ์ถ - ๋ ์ด์์ JSON์ ๋ชจ๋
nameํ๋๋ฅผ ๋งค๋ํ์คํธ์ ๋์กฐ - ์ฌ๊ท์ ์ผ๋ก
children๋ฐฐ์ด ๊ฒ์ฆ
์บ์ฑ ์ ๋ต:
- ์บ์ ํค:
template.{template_id}.components_manifest - ์บ์ TTL: 1์๊ฐ (
CACHE_TTL = 3600)
ํ์ผ: app/Rules/WhitelistedEndpoint.php
๋ชฉ์ : API ์๋ํฌ์ธํธ ํ์ดํธ๋ฆฌ์คํธ ๊ฒ์ฆ
ํ์ฉ ํจํด:
^/api/(admin|auth|public)/๊ฒ์ฆ ๋์:
data_sources[].endpointโ ๋ฐ์ดํฐ์์ค ์๋ํฌ์ธํธcomponents[].actions[].endpointโ ์ก์ ํธ๋ค๋ฌ ์๋ํฌ์ธํธ (์ฌ๊ท์ children ํฌํจ)
์ฐจ๋จ:
- ์ธ๋ถ URL (
http://,https://) - ๋น๊ณต๊ฐ API (
/api/internal/*) - ์ง์ ๊ฒฝ๋ก (
/admin/*) - ๊ฒฝ๋ก ํธ๋๋ฒ์ค:
../,..\ํจํด ์ฐจ๋จ
ํ์ผ: app/Rules/NoExternalUrls.php
๋ชฉ์ : ์ธ๋ถ URL ์ฐจ๋จ์ผ๋ก ๋ฐ์ดํฐ ์ ์ถ ๋ฐ ์ ์์ ๋ฆฌ์์ค ๋ก๋ฉ ๋ฐฉ์ง
์ฐจ๋จ ๋์ โ 7๊ฐ ์ํ URI ์คํด:
| ์คํด | ์ํ์ฑ |
|---|---|
http:// |
์ธ๋ถ ์๋ฒ ํต์ , ๋ฐ์ดํฐ ์ ์ถ |
https:// |
์ธ๋ถ ์๋ฒ ํต์ , ๋ฐ์ดํฐ ์ ์ถ |
data: |
์ธ๋ผ์ธ ๋ฐ์ดํฐ ์ฝ์ , XSS ๋ฒกํฐ |
javascript: |
์์ JavaScript ์คํ |
vbscript: |
์์ VBScript ์คํ (IE) |
file: |
๋ก์ปฌ ํ์ผ ์์คํ ์ ๊ทผ |
ftp: |
์ธ๋ถ FTP ์๋ฒ ํต์ |
์ถ๊ฐ ์ฐจ๋จ: //๋ก ์์ํ๋ ํ๋กํ ์ฝ ์๋ URL
๊ฒ์ฆ ๋ฒ์: components[] โ props, actions ๋ด ๋ชจ๋ ๋ฌธ์์ด ๊ฐ์ ์ฌ๊ท์ ์ผ๋ก ์ค์บ
ํ์ผ: app/Rules/ValidParentLayout.php
๋ชฉ์ : ๋ ์ด์์ ์์ ์ฒด๊ณ์ ์์ ์ฑ ๋ณด์ฅ
๊ฒ์ฆ ํญ๋ชฉ:
- ๋ถ๋ชจ ์กด์ฌ ํ์ธ: DB์์
template_id+name์กฐํ - ์ํ ์ฐธ์กฐ ๊ฐ์ง: ์ฌ๊ท์ ์ฒด์ธ ์ถ์ (
visited๋ฐฐ์ด๋ก ๋ฐฉ๋ฌธ ๋ ธ๋ ๊ธฐ๋ก) - ์์ ๊น์ด ์ ํ:
MAX_INHERITANCE_DEPTH = 10
์์ธ: ๋ ์ด์์ ์์ ๋ณด์
ํ์ผ: app/Rules/ValidSlotStructure.php
๋ชฉ์ : ๋ถ๋ชจ ๋ ์ด์์์์ ์ ์๋ ์ฌ๋กฏ๋ง ํ์ฉ
๊ฒ์ฆ ๋ก์ง:
extendsํ๋๊ฐ ์์ผ๋ฉด ๊ฒ์ฆ ํต๊ณผ- ๋ถ๋ชจ ๋ ์ด์์์
components์์{ "slot": "name" }ํจํด์ ์ฌ๊ท์ ์ผ๋ก ์์ง - ์์ ๋ ์ด์์์
slotsํค๊ฐ ๋ถ๋ชจ์ ์ ์๋ ์ฌ๋กฏ ์ด๋ฆ๊ณผ ์ผ์นํ๋์ง ๊ฒ์ฆ - ๋ถ๋ชจ์ ์๋ ์ฌ๋กฏ ์ด๋ฆ ์ฌ์ฉ ์ ์ฐจ๋จ
ํ์ผ: app/Rules/ValidDataSourceMerge.php
๋ชฉ์ : ์์ ์ฒด์ธ ์ ์ฒด์์ ๋ฐ์ดํฐ์์ค ID ๊ณ ์ ์ฑ ๋ณด์ฅ
๊ฒ์ฆ ๋ก์ง:
- ํ์ฌ ๋ ์ด์์ ๋ด
data_sources[].id์ค๋ณต ๊ฒ์ฌ extends๊ฐ ์์ผ๋ฉด ๋ถ๋ชจ ์ฒด์ธ ์ ์ฒด๋ฅผ ์ํํ์ฌ ๋ชจ๋ ๋ฐ์ดํฐ์์ค ID ์์ง- ํ์ฌ ๋ ์ด์์ ID์ ๋ถ๋ชจ ์ฒด์ธ ID์ ๊ต์งํฉ ๊ฒ์ฌ โ ์ค๋ณต ์ ์ฐจ๋จ
- ๋ฌดํ ๋ฃจํ ๋ฐฉ์ง (
visited๋ฐฐ์ด)
ํ์ผ: app/Rules/ValidPermissionStructure.php
๋ชฉ์ : ๊ถํ ๊ตฌ์กฐ์ ํ์์ ์ ํจ์ฑ ๊ฒ์ฆ
์ง์ ๊ตฌ์กฐ:
- Flat array:
["perm.read", "perm.write"] - OR ๊ตฌ์กฐ:
{ "or": ["perm.read", "perm.write"] }(ํ๋๋ผ๋ ๋ง์กฑ) - AND ๊ตฌ์กฐ:
{ "and": ["perm.read", "perm.write"] }(๋ชจ๋ ๋ง์กฑ) - ์ค์ฒฉ ๊ตฌ์กฐ:
{ "or": ["perm.a", { "and": ["perm.b", "perm.c"] }] }
์ ํ:
- ์ต๋ ์ค์ฒฉ ๊น์ด: 3๋จ๊ณ (
MAX_DEPTH = 3) - or/and ์ฐ์ฐ์์ ์ต์ 2๊ฐ ํญ๋ชฉ ํ์ (
MIN_OPERATOR_ITEMS = 2) - ๊ถํ ์๋ณ์ ์ ๊ท์:
/^[a-z0-9_-]+\.[a-z0-9_-]+(\.[a-z0-9_-]+)*$/i
์ฌ์ฉ ์์น: ๋ ์ด์์ ์ต์์ permissions + ์ปดํฌ๋ํธ ๋ ๋ฒจ permissions (ValidLayoutStructure์์ ์์)
ํ์ผ: app/Rules/SafeTemplatePath.php
๋ชฉ์ : ํ ํ๋ฆฟ ํ์ผ ๊ฒฝ๋ก์ Path Traversal ๊ณต๊ฒฉ ๋ฐฉ์ง
ํ์ผ: app/Rules/AllowedTemplateFileType.php
๋ชฉ์ : ํ์ฉ๋ ํ์ผ ํ์ฅ์๋ง ํต๊ณผ
14๊ฐ ํ์ฉ ํ์ฅ์:
| ์นดํ ๊ณ ๋ฆฌ | ํ์ฅ์ |
|---|---|
| Scripts | js, mjs |
| Styles | css |
| Data | json |
| Images | png, jpg, jpeg, svg, webp, gif |
| Fonts | woff, woff2, ttf, otf, eot |
๊ทธ ์ธ ๋ชจ๋ ํ์ฅ์ ์ฐจ๋จ (PHP, HTML, EXE ๋ฑ)
G7์ ๋ ์ด์์ CRUD ์์ ๋ณ๋ก 7๊ฐ FormRequest๋ฅผ ์ด์ฉํฉ๋๋ค. ๊ฐ FormRequest๋ ์ฉ๋์ ๋ง๋ Rule ์กฐํฉ์ ์ ์ฉํฉ๋๋ค.
| FormRequest | ์ฉ๋ | VLS | CE | WE | NEU | VPL | VSS | VDM | VPS |
|---|---|---|---|---|---|---|---|---|---|
| StoreLayoutRequest | ์ ๋ ์ด์์ ์์ฑ | โ | โ | โ | โ | ||||
| UpdateLayoutRequest | ๋ฉํ+์ฝํ ์ธ ์์ | โ * | โ * | โ * | โ * | ||||
| UpdateLayoutContentRequest | ์ฝํ ์ธ ๋ง ์์ | โ | โ | โ | โ | โ | โ | โ | |
| StoreLayoutInheritanceRequest | ์์ ๋ ์ด์์ ์์ฑ | โ | โ | โ | |||||
| UpdateLayoutInheritanceRequest | ์์ ๋ ์ด์์ ์์ | โ | โ | โ | |||||
| StoreLayoutPreviewRequest | ๋ฏธ๋ฆฌ๋ณด๊ธฐ | ||||||||
| GetLayoutRequest | ๋ ์ด์์ ์กฐํ |
๋ฒ๋ก: VLS=ValidLayoutStructure, CE=ComponentExists, WE=WhitelistedEndpoint, NEU=NoExternalUrls, VPL=ValidParentLayout, VSS=ValidSlotStructure, VDM=ValidDataSourceMerge, VPS=ValidPermissionStructure
โ
* = sometimes ๊ท์น (ํ๋๊ฐ ์กด์ฌํ ๋๋ง ์ ์ฉ)
- StoreLayoutRequest / UpdateLayoutRequest: ๊ธฐ๋ณธ 4๊ฐ Rule (๊ตฌ์กฐ, ์ปดํฌ๋ํธ, ์๋ํฌ์ธํธ, URL)
- UpdateLayoutContentRequest: ๊ฐ์ฅ ์๊ฒฉ โ ์์ ๊ด๋ จ 4๊ฐ Rule ์ถ๊ฐ (์ด 8๊ฐ)
- StoreLayoutInheritanceRequest / UpdateLayoutInheritanceRequest: ์์ ์ ์ฉ 3๊ฐ Rule
- StoreLayoutPreviewRequest: ์ต์ ๊ฒ์ฆ (
content: required, array) - GetLayoutRequest: ์กฐํ ์ ์ฉ (๋ณด์ Rule ์์)
G7 ๋ ์ด์์์ extends/slots/partial ์์คํ
์ผ๋ก ์์์ ์ง์ํฉ๋๋ค. ์์ ์ฒด๊ณ์ ์์ ์ฑ์ 3๊ฐ Rule์ด ํ๋ ฅํ์ฌ ๋ณด์ฅํฉ๋๋ค.
A extends B โ B extends C โ C extends A โ ์ฐจ๋จ!
๋ฉ์ปค๋์ฆ:
extendsํ๋์ ๋ถ๋ชจ ๋ ์ด์์์ DB์์ ์กฐํ- ๋ถ๋ชจ์
extends๋ฅผ ์ฌ๊ท์ ์ผ๋ก ์ถ์ (visited๋ฐฐ์ด๋ก ๋ฐฉ๋ฌธ ๊ธฐ๋ก) - ์ด๋ฏธ ๋ฐฉ๋ฌธํ ๋ ์ด์์ ๋๋ ์๊ธฐ ์์ ์ ์ฐธ์กฐํ๋ฉด ์ํ ์ฐธ์กฐ๋ก ํ์
- ์ต๋ ์์ ๊น์ด: 10๋จ๊ณ โ ์ด๊ณผ ์ ์ฐจ๋จ
๋ถ๋ชจ ๋ ์ด์์์ { "slot": "header" }, { "slot": "content" } ์ ์
โ
์์ ๋ ์ด์์์์ slots: { "header": [...], "footer": [...] }
โ ์ฐจ๋จ! (๋ถ๋ชจ์ "footer" ์ฌ๋กฏ ๋ฏธ์ ์)
- ๋ถ๋ชจ
componentsํธ๋ฆฌ๋ฅผ ์ฌ๊ท ํ์ํ์ฌ{ "slot": "name" }ํจํด ์์ง - ์์์
slotsํค๊ฐ ๋ถ๋ชจ ์ฌ๋กฏ ๋ชฉ๋ก์ ํฌํจ๋๋์ง ๊ฒ์ฆ
์กฐ๋ถ๋ชจ: data_sources: [{ id: "users" }]
๋ถ๋ชจ: data_sources: [{ id: "posts" }]
์์: data_sources: [{ id: "users" }] โ ์ฐจ๋จ! (์กฐ๋ถ๋ชจ์ ID ์ถฉ๋)
- ์์ ์ฒด์ธ ์ ์ฒด๋ฅผ ์ํํ์ฌ ๋ชจ๋ ๋ฐ์ดํฐ์์ค ID ์์ง
- ํ์ฌ ๋ ์ด์์ ID์์ ๊ต์งํฉ ๊ฒ์ฌ
- ๋ฌดํ ๋ฃจํ ๋ฐฉ์ง ๋ก์ง ํฌํจ
ํ์ผ: app/Rules/SafeTemplatePath.php
G7์ OS ๋ ๋ฒจ ํ์ผ ๊ถํ์ ์์กดํ์ง ์๊ณ , ์ ํ๋ฆฌ์ผ์ด์ ๋ ๋ฒจ์์ ๊ฒฝ๋ก๋ฅผ ์ ๊ทน์ ์ผ๋ก ๊ฒ์ฆํฉ๋๋ค.
| ํจํด | ์ค๋ช |
|---|---|
../ |
Unix ์์ ๋๋ ํ ๋ฆฌ |
..\ |
Windows ์์ ๋๋ ํ ๋ฆฌ |
// |
์ด์ค ์ฌ๋์ |
%2e%2e%2f |
URL ์ธ์ฝ๋ฉ ../ |
%2e%2e/ |
๋ถ๋ถ URL ์ธ์ฝ๋ฉ |
%2e%2e%5c |
URL ์ธ์ฝ๋ฉ ..\ |
%2e%2e\ |
๋ถ๋ถ URL ์ธ์ฝ๋ฉ |
..%2f |
ํผํฉ ์ธ์ฝ๋ฉ ../ |
..%5c |
ํผํฉ ์ธ์ฝ๋ฉ ..\ |
.%2e/ |
๋ถ๋ถ ์ธ์ฝ๋ฉ ../ |
.%2e\ |
๋ถ๋ถ ์ธ์ฝ๋ฉ ..\ |
// ์ต๋ 5ํ ๋ฐ๋ณต URL ๋์ฝ๋ฉ (์ด์ค/์ผ์ค ์ธ์ฝ๋ฉ ๊ณต๊ฒฉ ๋ฐฉ์ง)
for ($i = 0; $i < 5 && $decodedPath !== $previousPath; $i++) {
$previousPath = $decodedPath;
$decodedPath = urldecode($decodedPath);
}๊ณต๊ฒฉ์๊ฐ %252e%252e%252f (์ผ์ค ์ธ์ฝ๋ฉ)๋ฅผ ์ฌ์ฉํด๋ 5ํ ๋์ฝ๋ฉ์ผ๋ก ์๋ณธ ํจํด์ด ๋๋ฌ๋จ
| ๋ฐฉ์ด ํญ๋ชฉ | ์ค๋ช |
|---|---|
| ์ ๋ ๊ฒฝ๋ก ์ฐจ๋จ | /, \, C: ๋ฑ ์ ๋ ๊ฒฝ๋ก ์์ ํจํด ์ฐจ๋จ (Windows/Linux ๋ชจ๋) |
| NULL ๋ฐ์ดํธ ์ฐจ๋จ | \0 ๋ฌธ์ ๊ฐ์ง ์ ์ฐจ๋จ (C ์ธ์ด ๋ฌธ์์ด ์ข
๋ฃ ๊ณต๊ฒฉ) |
| basePath ์ธ๋ถ ์ ๊ทผ ์ฐจ๋จ | realpath() ์ ๊ทํ ํ basePath ์ธ๋ถ ๊ฒฝ๋ก ๊ฐ์ง ์ ์ฐจ๋จ |
๋ชจ๋/ํ๋ฌ๊ทธ์ธ์ 6๊ฐ Filter Hook์ ํตํด ๋ ์ด์์ ๊ฒ์ฆ ๊ท์น์ ๋์ ์ผ๋ก ์ถ๊ฐํ ์ ์์ต๋๋ค.
| Hook ์ด๋ฆ | FormRequest | ์ฉ๋ |
|---|---|---|
core.layout.store_validation_rules |
StoreLayoutRequest | ๋ ์ด์์ ์์ฑ ์ ๊ท์น ์ถ๊ฐ |
core.layout.update_validation_rules |
UpdateLayoutRequest | ๋ ์ด์์ ์์ ์ ๊ท์น ์ถ๊ฐ |
core.layout.update_content_validation_rules |
UpdateLayoutContentRequest | ์ฝํ ์ธ ์์ ์ ๊ท์น ์ถ๊ฐ |
core.layout.store_inheritance_validation_rules |
StoreLayoutInheritanceRequest | ์์ ๋ ์ด์์ ์์ฑ ์ ๊ท์น ์ถ๊ฐ |
core.layout.update_inheritance_validation_rules |
UpdateLayoutInheritanceRequest | ์์ ๋ ์ด์์ ์์ ์ ๊ท์น ์ถ๊ฐ |
core.layout.get_validation_rules |
GetLayoutRequest | ๋ ์ด์์ ์กฐํ ์ ๊ท์น ์ถ๊ฐ |
// ๋ชจ๋์ Listener์์ ์ปค์คํ
๊ฒ์ฆ ๊ท์น ์ถ๊ฐ
class LayoutValidationListener
{
public function handle(array $rules, StoreLayoutRequest $request): array
{
// ๋ชจ๋ ์ ์ฉ ๊ฒ์ฆ ๊ท์น ์ถ๊ฐ
$rules['content'][] = new MyCustomRule();
return $rules;
}
}// ๋ชจ๋์ ServiceProvider์์ Hook ๋ฑ๋ก
HookManager::addFilter(
'core.layout.store_validation_rules',
[LayoutValidationListener::class, 'handle'],
priority: 10
);์ฐธ๊ณ : Filter ํ ์ด๋ฏ๋ก ๋ฆฌ์ค๋์์
type: 'filter'๋ช ์ ํ์ (๋ฏธ์ง์ ์ ๋ฐํ๊ฐ ๋ฌด์) ์์ธ: ํ ์์คํ
| ๊ณต๊ฒฉ ์ ํ | ๊ณต๊ฒฉ ๋ฒกํฐ | ๋ฐฉ์ด ๋ฉ์ปค๋์ฆ | ์ฐจ๋จ ๊ณ์ธต |
|---|---|---|---|
| XSS | <script> ํ๊ทธ ์ฝ์
|
React ์๋ ์ด์ค์ผ์ดํ | ํ๋ก ํธ์๋ |
| SQL Injection | '; DROP TABLE-- |
Eloquent ORM ํ๋ผ๋ฏธํฐ ๋ฐ์ธ๋ฉ | Repository |
| CSRF | ํ ํฐ ์๋ ์์ฒญ | Laravel Sanctum Bearer ํ ํฐ | ๋ฏธ๋ค์จ์ด |
| ๊ฒฝ๋ก ํธ๋๋ฒ์ค | ../../etc/passwd |
SafeTemplatePath (13ํจํด + 5ํ ๋์ฝ๋ฉ) | FormRequest |
| ๋ค์ค ์ธ์ฝ๋ฉ ๊ฒฝ๋ก ํธ๋๋ฒ์ค | %252e%252e%252f |
SafeTemplatePath 5ํ ๋ฐ๋ณต ๋์ฝ๋ฉ | FormRequest |
| NULL ๋ฐ์ดํธ ๊ฒฝ๋ก ๊ณต๊ฒฉ | file.php\0.jpg |
SafeTemplatePath NULL ๋ฐ์ดํธ ๊ฐ์ง | FormRequest |
| ์ธ๋ถ ๋ฆฌ์์ค ๋ก๋ฉ | https://evil.com |
NoExternalUrls (7๊ฐ ์คํด) | FormRequest |
| XSS via URI | javascript:alert(1) |
NoExternalUrls (javascript: ์ฐจ๋จ) |
FormRequest |
| Data URI ์ฝ์ | data:text/html,... |
NoExternalUrls (data: ์ฐจ๋จ) |
FormRequest |
| ํ๋กํ ์ฝ ์๋ URL | //evil.com/malicious.js |
NoExternalUrls (// ์ฐจ๋จ) |
FormRequest |
| ํ์ฉ๋์ง ์์ API | /api/internal/ |
WhitelistedEndpoint | FormRequest |
| API ๊ฒฝ๋ก ํธ๋๋ฒ์ค | /api/admin/../internal/ |
WhitelistedEndpoint (../ ์ฐจ๋จ) |
FormRequest |
| ์กด์ฌํ์ง ์๋ ์ปดํฌ๋ํธ | MaliciousComponent |
ComponentExists (๋งค๋ํ์คํธ ๋์กฐ) | FormRequest |
| DoS (๊น์ ์ค์ฒฉ) | 11๋จ๊ณ ์ด์ | ValidLayoutStructure (MAX_DEPTH = 10) |
FormRequest |
| DoS (์์ ๊น์ด) | 11๋จ๊ณ ์ด์ | ValidParentLayout (MAX_INHERITANCE_DEPTH = 10) |
FormRequest |
| DoS (๊ถํ ์ค์ฒฉ) | 4๋จ๊ณ ์ด์ | ValidPermissionStructure (MAX_DEPTH = 3) |
FormRequest |
| ์์ ์ํ ์ฐธ์กฐ | AโBโCโA | ValidParentLayout (visited ์ถ์ ) | FormRequest |
| ์ ๋ น ์ฌ๋กฏ ์ฃผ์ | ๋ถ๋ชจ์ ์๋ ์ฌ๋กฏ | ValidSlotStructure | FormRequest |
| ๋ฐ์ดํฐ์์ค ID ์ถฉ๋ | ๋ถ๋ชจ์ ๋์ผ ID | ValidDataSourceMerge (์ฒด์ธ ์ ์ฒด ๊ฒ์ฌ) | FormRequest |
| ์ํ ํ์ผ ์ ๋ก๋ | malicious.php |
AllowedTemplateFileType (14๊ฐ ํ์ดํธ๋ฆฌ์คํธ) | FormRequest |
์ ์์ ์ฌ์ฉ์๊ฐ ์ฌ๋ฌ ๊ณต๊ฒฉ ๊ธฐ๋ฒ์ ์กฐํฉํ๋๋ผ๋, ๊ฐ ๊ณ์ธต์์ ๋ ๋ฆฝ์ ์ผ๋ก ๊ฒ์ฆํ๋ฏ๋ก ํ๋์ ๊ณต๊ฒฉ์ด ํต๊ณผํ๋๋ผ๋ ๋ค์ ๊ณ์ธต์์ ์ฐจ๋จ๋ฉ๋๋ค.
{
"version": "1.0.0",
"layout_name": "combined_attack",
"data_sources": [
{
"endpoint": "/api/internal/secrets",
"params": { "filter": "'; DROP TABLE--" }
}
],
"components": [
{
"name": "NonExistentComponent",
"type": "basic",
"props": {
"imageUrl": "https://evil.com/image.jpg",
"onClick": "<script>alert(1)</script>"
}
}
]
}๊ฒฐ๊ณผ: FormRequest์ ์ฒซ ๋ฒ์งธ ๊ฒ์ฆ ๊ท์น์์ ์ฐจ๋จ (422 ์๋ต)
WhitelistedEndpoint:/api/internal/์ฐจ๋จComponentExists:NonExistentComponent์ฐจ๋จNoExternalUrls:https://evil.com์ฐจ๋จ- React:
<script>ํ๊ทธ๋ ์๋ ์ด์ค์ผ์ดํ (ํ ์คํธ๋ก ํ์) - Eloquent ORM: SQL ๊ตฌ๋ฌธ์ ํ๋ผ๋ฏธํฐ ๋ฐ์ธ๋ฉ์ผ๋ก ์์ ์ฒ๋ฆฌ
- ์ปดํฌ๋ํธ ๋ช
์ธ ๋ฌธ์ํ:
components.json์ ๋ชจ๋ ์ปดํฌ๋ํธ๋ฅผbasic/composite/layout์นดํ ๊ณ ๋ฆฌ๋ก ๋ถ๋ฅ ๋ฑ๋ก - ์ธ๋ถ ์์กด์ฑ ์ต์ํ: ์ธ๋ถ CDN ์ฌ์ฉ ์์ ,
/public/build/template/๋๋ ํ ๋ฆฌ์ ๋ฒ๋ค๋ง - props ํ์ ๊ฒ์ฆ: ์ปดํฌ๋ํธ ๋ด๋ถ์์ TypeScript๋ก props ํ์ ๊ฒ์ฆ
- Hook์ ํตํ ๊ฒ์ฆ ์ถ๊ฐ: ๋ชจ๋ ์ ์ฉ ๊ฒ์ฆ์ด ํ์ํ๋ฉด
core.layout.*_validation_rulesHook ์ฌ์ฉ - Custom Rule ์์ฑ:
Illuminate\Contracts\Validation\ValidationRule์ธํฐํ์ด์ค ๊ตฌํ __()ํจ์ ์ฌ์ฉ: Custom Rule ์๋ฌ ๋ฉ์์ง๋ ๋ฐ๋์ ๋ค๊ตญ์ด ์ฒ๋ฆฌ
- ์ ๊ธฐ์ ์ธ ๋ณด์ ๊ฐ์ฌ:
MaliciousJsonTest.php์คํ์ผ๋ก ๋ฐฉ์ด ์ฒด๊ณ ๊ฒ์ฆ - ๋ก๊ทธ ๋ชจ๋ํฐ๋ง: 422 ์๋ต ๋น๋ ๋ชจ๋ํฐ๋ง, ๋ฐ๋ณต์ ์ธ ์ ์์ ์๋ IP ์ฐจ๋จ
- ํ๊ฒฝ ๋ณ์ ๋ณดํธ:
.envํ์ผ ๊ถํ ์ ํ (600), API ํค/DB ๋น๋ฐ๋ฒํธ ์์ ๋ณด๊ด - ์ ๋ฐ์ดํธ ๊ด๋ฆฌ: Laravel ๋ฐ ์์กด์ฑ ํจํค์ง ์ต์ ๋ฒ์ ์ ์ง
- ๋ชจ๋ ๊ฒ์ฆ ๋ก์ง์ด FormRequest์ ์๋๊ฐ?
- Service์ ๊ฒ์ฆ ๋ก์ง์ด ์๋๊ฐ?
- Custom Rule์์
__()ํจ์๋ก ๋ค๊ตญ์ด ์ฒ๋ฆฌํ๋๊ฐ? - ์ธ๋ถ URL ์ฐธ์กฐ๊ฐ ์๋๊ฐ?
-
dangerouslySetInnerHTML์ฌ์ฉ์ด ์๋๊ฐ? - DB ์ฟผ๋ฆฌ๊ฐ Eloquent ORM์ ์ฌ์ฉํ๋๊ฐ?
- API ๋ผ์ฐํธ์ ์ธ์ฆ ๋ฏธ๋ค์จ์ด๊ฐ ์๋๊ฐ?
- ์๋ก์ด ์๋ํฌ์ธํธ๊ฐ WhitelistedEndpoint ํจํด์ ๋ถํฉํ๋๊ฐ?
- ๋ ์ด์์ ์์ ์ ๋ถ๋ชจ ์ฌ๋กฏ๊ณผ ๋ฐ์ดํฐ์์ค ID ์ถฉ๋์ด ์๋๊ฐ?
- ํ๋ก ํธ์๋ ๋ณด์ ๋ฐ ๊ฒ์ฆ โ XSS, ํํ์, ์ํ ๋ ธ์ถ, ์ธ์ฆ, ์์ ๋ณด์
- ํ ํ๋ฆฟ ๋ณด์ ์ ์ฑ โ ํ ํ๋ฆฟ ์์ ์๋น, ๊ฒฝ๋ก ๋ณด์
- ๊ฒ์ฆ (Validation) โ FormRequest + Custom Rule ํจํด
- ์ธ์ฆ ๋ฐ ์ธ์ ์ฒ๋ฆฌ โ Sanctum ํ ํฐ ๊ธฐ๋ฐ ์ธ์ฆ
- Service-Repository ํจํด โ ๊ณ์ธต ๋ถ๋ฆฌ
- ํ ์์คํ โ Action/Filter Hook, ๋ชจ๋/ํ๋ฌ๊ทธ์ธ ํ์ฅ
- ๋ ์ด์์ ํ์ฅ ์์คํ โ ๋ ์ด์์ ๋์ ์ฃผ์
- ๋ ์ด์์ JSON ์คํค๋ง โ JSON ๊ตฌ์กฐ ๋ช ์ธ
- ๋ ์ด์์ JSON - ์์ โ extends/slots/partial ์์คํ
- ์ธ์ฆ ์์คํ (AuthManager) โ ํ๋ก ํธ์๋ ์ธ์ฆ ํ๋ฆ
- Custom Rules:
app/Rules/*.php(10๊ฐ ๋ณด์ ๊ด๋ จ) - FormRequests:
app/Http/Requests/Layout/*.php(7๊ฐ) - ํตํฉ ํ
์คํธ:
tests/Feature/Security/MaliciousJsonTest.php - ๋ค๊ตญ์ด ํ์ผ:
lang/ko/validation.php,lang/en/validation.php
๋ง์ง๋ง ์ ๋ฐ์ดํธ: 2026-03-30