Skip to content

Security: gnuboard/g7

Security

docs/SECURITY.md

๊ทธ๋ˆ„๋ณด๋“œ7 ํ…œํ”Œ๋ฆฟ ์—”์ง„ ๋ณด์•ˆ ๊ฐ€์ด๋“œ

๊ทธ๋ˆ„๋ณด๋“œ7 ๋ ˆ์ด์•„์›ƒ JSON ๋ณด์•ˆ ์•„ํ‚คํ…์ฒ˜ โ€” Custom Validation Rules, FormRequest ๊ฒ€์ฆ ๊ณ„์ธต, ๊ณต๊ฒฉ ๋ฐฉ์–ด ์ „๋žต


TL;DR (5์ดˆ ์š”์•ฝ)

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์œผ๋กœ ๋ชจ๋“ˆ/ํ”Œ๋Ÿฌ๊ทธ์ธ์ด ๊ฒ€์ฆ ๊ทœ์น™์„ ๋™์ ์œผ๋กœ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ

๋ชฉ์ฐจ

  1. ๊ฐœ์š”
  2. ๋ณด์•ˆ ์•„ํ‚คํ…์ฒ˜
  3. ๊ฒ€์ฆ ๊ณ„์ธต
  4. Custom Validation Rules
  5. FormRequest๋ณ„ Rule ์ ์šฉ ํ˜„ํ™ฉ
  6. ๋ ˆ์ด์•„์›ƒ ์ƒ์† ๋ณด์•ˆ
  7. ๊ฒฝ๋กœ ๋ณด์•ˆ (Path Traversal ๋ฐฉ์–ด)
  8. Hook ๊ธฐ๋ฐ˜ ๊ฒ€์ฆ ํ™•์žฅ
  9. ๊ณต๊ฒฉ ๋ฐฉ์–ด ์ „๋žต
  10. ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€
  11. ๊ด€๋ จ ๋ฌธ์„œ

๊ฐœ์š”

๊ทธ๋ˆ„๋ณด๋“œ7 ํ…œํ”Œ๋ฆฟ ์—”์ง„์€ JSON ๊ธฐ๋ฐ˜ ๋ ˆ์ด์•„์›ƒ์œผ๋กœ ํ™”๋ฉด์„ ๋™์ ์œผ๋กœ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด ์œ ์—ฐ์„ฑ์€ ์•…์˜์  ์ž…๋ ฅ์„ ํ†ตํ•œ ๊ณต๊ฒฉ ๊ฐ€๋Šฅ์„ฑ์„ ๋‚ดํฌํ•˜๋ฏ€๋กœ, 10๊ฐœ Custom Validation Rule๊ณผ 7๊ฐœ FormRequest๋ฅผ ์กฐํ•ฉํ•œ ๋‹ค์ธต ๋ฐฉ์–ด ์ฒด๊ณ„๋กœ ๋ณดํ˜ธํ•ฉ๋‹ˆ๋‹ค.

๋ณด์•ˆ ์›์น™

  1. ๋‹ค์ธต ๋ฐฉ์–ด (Defense in Depth): FormRequest โ†’ Service โ†’ Repository โ†’ ํ”„๋ก ํŠธ์—”๋“œ, ๊ฐ ๊ณ„์ธต์—์„œ ๋…๋ฆฝ์  ๊ฒ€์ฆ
  2. ์ตœ์†Œ ๊ถŒํ•œ ์›์น™ (Principle of Least Privilege): ํ•„์š”ํ•œ ์ตœ์†Œํ•œ์˜ ๊ถŒํ•œ๋งŒ ๋ถ€์—ฌ
  3. ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๋ฐฉ์‹ (Whitelist Approach): ๋ช…์‹œ์ ์œผ๋กœ ํ—ˆ์šฉ๋œ ๊ฒƒ๋งŒ ํ†ต๊ณผ
  4. ๊ฒ€์ฆ ์šฐ์„  (Fail Secure): ๊ฒ€์ฆ ์‹คํŒจ ์‹œ ์•ˆ์ „ํ•œ ์ƒํƒœ๋กœ ๋ณต๊ท€
  5. ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๊ฒ€์ฆ (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                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๊ฒ€์ฆ ๊ณ„์ธต

1๋‹จ๊ณ„: FormRequest ๊ฒ€์ฆ (Controller ์ง„์ž… ์ „)

์œ„์น˜: app/Http/Requests/Layout/

๋ชฉ์ : Controller์— ๋„๋‹ฌํ•˜๊ธฐ ์ „์— ๋ชจ๋“  ์•…์˜์  ์ž…๋ ฅ์„ ์ฐจ๋‹จ

G7์€ ์šฉ๋„๋ณ„ 7๊ฐœ FormRequest๋กœ ๊ฒ€์ฆ์„ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๊ฐ FormRequest๋Š” ์šฉ๋„์— ๋งž๋Š” Custom Rule ์กฐํ•ฉ์„ ์ ์šฉํ•˜๋ฉฐ, Hook์„ ํ†ตํ•ด ๋ชจ๋“ˆ/ํ”Œ๋Ÿฌ๊ทธ์ธ์ด ๊ทœ์น™์„ ํ™•์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ƒ์„ธ: FormRequest๋ณ„ Rule ์ ์šฉ ํ˜„ํ™ฉ

2๋‹จ๊ณ„: Service ๊ณ„์ธต (๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง)

์œ„์น˜: app/Services/LayoutService.php

ํŠน์ง•:

  • ๊ฒ€์ฆ ๋กœ์ง ์—†์Œ (FormRequest์—์„œ ์™„๋ฃŒ)
  • ์ˆœ์ˆ˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๋งŒ ์ฒ˜๋ฆฌ
  • ํ›… ์‹œ์Šคํ…œ์„ ํ†ตํ•œ ํ™•์žฅ์„ฑ (before_save, after_save)

3๋‹จ๊ณ„: Repository ๊ณ„์ธต (๋ฐ์ดํ„ฐ ์ ‘๊ทผ)

์œ„์น˜: app/Repositories/LayoutRepository.php

ํŠน์ง•:

  • Eloquent ORM ์‚ฌ์šฉ (ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ์œผ๋กœ SQL Injection ์ž๋™ ๋ฐฉ์–ด)
  • ์ง์ ‘ SQL ๋ฌธ์ž์—ด ์—ฐ๊ฒฐ ๊ธˆ์ง€

Custom Validation Rules

G7์€ ๋ ˆ์ด์•„์›ƒ JSON ๋ณด์•ˆ์„ ์œ„ํ•ด 10๊ฐœ์˜ Custom Validation Rule์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

1. ValidLayoutStructure

ํŒŒ์ผ: 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 ํ•„๋“œ: ValidPermissionStructure Rule์— ์œ„์ž„
  • ์Šฌ๋กฏ ์ฐธ์กฐ({ "slot": "name" })์™€ Partial ์ฐธ์กฐ({ "partial": "path" }) ํ—ˆ์šฉ

2. ComponentExists

ํŒŒ์ผ: app/Rules/ComponentExists.php

๋ชฉ์ : ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ปดํฌ๋„ŒํŠธ ์ฐธ์กฐ ๋ฐฉ์ง€

๊ฒ€์ฆ ๋ฐฉ๋ฒ•:

  1. ํ…œํ”Œ๋ฆฟ์˜ components.json ๋งค๋‹ˆํŽ˜์ŠคํŠธ ๋กœ๋“œ
  2. 3๊ฐœ ์นดํ…Œ๊ณ ๋ฆฌ: basic, composite, layout ์ปดํฌ๋„ŒํŠธ๋ฅผ Set์œผ๋กœ ๊ตฌ์ถ•
  3. ๋ ˆ์ด์•„์›ƒ JSON์˜ ๋ชจ๋“  name ํ•„๋“œ๋ฅผ ๋งค๋‹ˆํŽ˜์ŠคํŠธ์™€ ๋Œ€์กฐ
  4. ์žฌ๊ท€์ ์œผ๋กœ children ๋ฐฐ์—ด ๊ฒ€์ฆ

์บ์‹ฑ ์ „๋žต:

  • ์บ์‹œ ํ‚ค: template.{template_id}.components_manifest
  • ์บ์‹œ TTL: 1์‹œ๊ฐ„ (CACHE_TTL = 3600)

3. WhitelistedEndpoint

ํŒŒ์ผ: app/Rules/WhitelistedEndpoint.php

๋ชฉ์ : API ์—”๋“œํฌ์ธํŠธ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๊ฒ€์ฆ

ํ—ˆ์šฉ ํŒจํ„ด:

^/api/(admin|auth|public)/

๊ฒ€์ฆ ๋Œ€์ƒ:

  • data_sources[].endpoint โ€” ๋ฐ์ดํ„ฐ์†Œ์Šค ์—”๋“œํฌ์ธํŠธ
  • components[].actions[].endpoint โ€” ์•ก์…˜ ํ•ธ๋“ค๋Ÿฌ ์—”๋“œํฌ์ธํŠธ (์žฌ๊ท€์  children ํฌํ•จ)

์ฐจ๋‹จ:

  • ์™ธ๋ถ€ URL (http://, https://)
  • ๋น„๊ณต๊ฐœ API (/api/internal/*)
  • ์ง์ ‘ ๊ฒฝ๋กœ (/admin/*)
  • ๊ฒฝ๋กœ ํŠธ๋ž˜๋ฒ„์„ค: ../, ..\ ํŒจํ„ด ์ฐจ๋‹จ

4. NoExternalUrls

ํŒŒ์ผ: app/Rules/NoExternalUrls.php

๋ชฉ์ : ์™ธ๋ถ€ URL ์ฐจ๋‹จ์œผ๋กœ ๋ฐ์ดํ„ฐ ์œ ์ถœ ๋ฐ ์•…์˜์  ๋ฆฌ์†Œ์Šค ๋กœ๋”ฉ ๋ฐฉ์ง€

์ฐจ๋‹จ ๋Œ€์ƒ โ€” 7๊ฐœ ์œ„ํ—˜ URI ์Šคํ‚ด:

์Šคํ‚ด ์œ„ํ—˜์„ฑ
http:// ์™ธ๋ถ€ ์„œ๋ฒ„ ํ†ต์‹ , ๋ฐ์ดํ„ฐ ์œ ์ถœ
https:// ์™ธ๋ถ€ ์„œ๋ฒ„ ํ†ต์‹ , ๋ฐ์ดํ„ฐ ์œ ์ถœ
data: ์ธ๋ผ์ธ ๋ฐ์ดํ„ฐ ์‚ฝ์ž…, XSS ๋ฒกํ„ฐ
javascript: ์ž„์˜ JavaScript ์‹คํ–‰
vbscript: ์ž„์˜ VBScript ์‹คํ–‰ (IE)
file: ๋กœ์ปฌ ํŒŒ์ผ ์‹œ์Šคํ…œ ์ ‘๊ทผ
ftp: ์™ธ๋ถ€ FTP ์„œ๋ฒ„ ํ†ต์‹ 

์ถ”๊ฐ€ ์ฐจ๋‹จ: //๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ”„๋กœํ† ์ฝœ ์ƒ๋Œ€ URL

๊ฒ€์ฆ ๋ฒ”์œ„: components[] โ†’ props, actions ๋‚ด ๋ชจ๋“  ๋ฌธ์ž์—ด ๊ฐ’์„ ์žฌ๊ท€์ ์œผ๋กœ ์Šค์บ”

5. ValidParentLayout

ํŒŒ์ผ: app/Rules/ValidParentLayout.php

๋ชฉ์ : ๋ ˆ์ด์•„์›ƒ ์ƒ์† ์ฒด๊ณ„์˜ ์•ˆ์ „์„ฑ ๋ณด์žฅ

๊ฒ€์ฆ ํ•ญ๋ชฉ:

  1. ๋ถ€๋ชจ ์กด์žฌ ํ™•์ธ: DB์—์„œ template_id + name ์กฐํšŒ
  2. ์ˆœํ™˜ ์ฐธ์กฐ ๊ฐ์ง€: ์žฌ๊ท€์  ์ฒด์ธ ์ถ”์  (visited ๋ฐฐ์—ด๋กœ ๋ฐฉ๋ฌธ ๋…ธ๋“œ ๊ธฐ๋ก)
  3. ์ƒ์† ๊นŠ์ด ์ œํ•œ: MAX_INHERITANCE_DEPTH = 10

์ƒ์„ธ: ๋ ˆ์ด์•„์›ƒ ์ƒ์† ๋ณด์•ˆ

6. ValidSlotStructure

ํŒŒ์ผ: app/Rules/ValidSlotStructure.php

๋ชฉ์ : ๋ถ€๋ชจ ๋ ˆ์ด์•„์›ƒ์—์„œ ์ •์˜๋œ ์Šฌ๋กฏ๋งŒ ํ—ˆ์šฉ

๊ฒ€์ฆ ๋กœ์ง:

  1. extends ํ•„๋“œ๊ฐ€ ์—†์œผ๋ฉด ๊ฒ€์ฆ ํ†ต๊ณผ
  2. ๋ถ€๋ชจ ๋ ˆ์ด์•„์›ƒ์˜ components์—์„œ { "slot": "name" } ํŒจํ„ด์„ ์žฌ๊ท€์ ์œผ๋กœ ์ˆ˜์ง‘
  3. ์ž์‹ ๋ ˆ์ด์•„์›ƒ์˜ slots ํ‚ค๊ฐ€ ๋ถ€๋ชจ์— ์ •์˜๋œ ์Šฌ๋กฏ ์ด๋ฆ„๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ๊ฒ€์ฆ
  4. ๋ถ€๋ชจ์— ์—†๋Š” ์Šฌ๋กฏ ์ด๋ฆ„ ์‚ฌ์šฉ ์‹œ ์ฐจ๋‹จ

7. ValidDataSourceMerge

ํŒŒ์ผ: app/Rules/ValidDataSourceMerge.php

๋ชฉ์ : ์ƒ์† ์ฒด์ธ ์ „์ฒด์—์„œ ๋ฐ์ดํ„ฐ์†Œ์Šค ID ๊ณ ์œ ์„ฑ ๋ณด์žฅ

๊ฒ€์ฆ ๋กœ์ง:

  1. ํ˜„์žฌ ๋ ˆ์ด์•„์›ƒ ๋‚ด data_sources[].id ์ค‘๋ณต ๊ฒ€์‚ฌ
  2. extends๊ฐ€ ์žˆ์œผ๋ฉด ๋ถ€๋ชจ ์ฒด์ธ ์ „์ฒด๋ฅผ ์ˆœํšŒํ•˜์—ฌ ๋ชจ๋“  ๋ฐ์ดํ„ฐ์†Œ์Šค ID ์ˆ˜์ง‘
  3. ํ˜„์žฌ ๋ ˆ์ด์•„์›ƒ ID์™€ ๋ถ€๋ชจ ์ฒด์ธ ID์˜ ๊ต์ง‘ํ•ฉ ๊ฒ€์‚ฌ โ†’ ์ค‘๋ณต ์‹œ ์ฐจ๋‹จ
  4. ๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€ (visited ๋ฐฐ์—ด)

8. ValidPermissionStructure

ํŒŒ์ผ: 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์—์„œ ์œ„์ž„)

9. SafeTemplatePath

ํŒŒ์ผ: app/Rules/SafeTemplatePath.php

๋ชฉ์ : ํ…œํ”Œ๋ฆฟ ํŒŒ์ผ ๊ฒฝ๋กœ์˜ Path Traversal ๊ณต๊ฒฉ ๋ฐฉ์ง€

์ƒ์„ธ: ๊ฒฝ๋กœ ๋ณด์•ˆ (Path Traversal ๋ฐฉ์–ด)

10. AllowedTemplateFileType

ํŒŒ์ผ: 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 ๋“ฑ)


FormRequest๋ณ„ Rule ์ ์šฉ ํ˜„ํ™ฉ

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์ด ํ˜‘๋ ฅํ•˜์—ฌ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

์ˆœํ™˜ ์ฐธ์กฐ ๋ฐฉ์ง€ (ValidParentLayout)

A extends B โ†’ B extends C โ†’ C extends A  โ† ์ฐจ๋‹จ!

๋ฉ”์ปค๋‹ˆ์ฆ˜:

  1. extends ํ•„๋“œ์˜ ๋ถ€๋ชจ ๋ ˆ์ด์•„์›ƒ์„ DB์—์„œ ์กฐํšŒ
  2. ๋ถ€๋ชจ์˜ extends๋ฅผ ์žฌ๊ท€์ ์œผ๋กœ ์ถ”์  (visited ๋ฐฐ์—ด๋กœ ๋ฐฉ๋ฌธ ๊ธฐ๋ก)
  3. ์ด๋ฏธ ๋ฐฉ๋ฌธํ•œ ๋ ˆ์ด์•„์›ƒ ๋˜๋Š” ์ž๊ธฐ ์ž์‹ ์„ ์ฐธ์กฐํ•˜๋ฉด ์ˆœํ™˜ ์ฐธ์กฐ๋กœ ํŒ์ •
  4. ์ตœ๋Œ€ ์ƒ์† ๊นŠ์ด: 10๋‹จ๊ณ„ โ€” ์ดˆ๊ณผ ์‹œ ์ฐจ๋‹จ

์Šฌ๋กฏ ์•ˆ์ „์„ฑ (ValidSlotStructure)

๋ถ€๋ชจ ๋ ˆ์ด์•„์›ƒ์— { "slot": "header" }, { "slot": "content" } ์ •์˜
                                    โ†“
์ž์‹ ๋ ˆ์ด์•„์›ƒ์—์„œ slots: { "header": [...], "footer": [...] }
                                                    โ†‘ ์ฐจ๋‹จ! (๋ถ€๋ชจ์— "footer" ์Šฌ๋กฏ ๋ฏธ์ •์˜)
  • ๋ถ€๋ชจ components ํŠธ๋ฆฌ๋ฅผ ์žฌ๊ท€ ํƒ์ƒ‰ํ•˜์—ฌ { "slot": "name" } ํŒจํ„ด ์ˆ˜์ง‘
  • ์ž์‹์˜ slots ํ‚ค๊ฐ€ ๋ถ€๋ชจ ์Šฌ๋กฏ ๋ชฉ๋ก์— ํฌํ•จ๋˜๋Š”์ง€ ๊ฒ€์ฆ

๋ฐ์ดํ„ฐ์†Œ์Šค ID ๊ณ ์œ ์„ฑ (ValidDataSourceMerge)

์กฐ๋ถ€๋ชจ: data_sources: [{ id: "users" }]
๋ถ€๋ชจ:   data_sources: [{ id: "posts" }]
์ž์‹:   data_sources: [{ id: "users" }]  โ† ์ฐจ๋‹จ! (์กฐ๋ถ€๋ชจ์™€ ID ์ถฉ๋Œ)
  • ์ƒ์† ์ฒด์ธ ์ „์ฒด๋ฅผ ์ˆœํšŒํ•˜์—ฌ ๋ชจ๋“  ๋ฐ์ดํ„ฐ์†Œ์Šค ID ์ˆ˜์ง‘
  • ํ˜„์žฌ ๋ ˆ์ด์•„์›ƒ ID์™€์˜ ๊ต์ง‘ํ•ฉ ๊ฒ€์‚ฌ
  • ๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€ ๋กœ์ง ํฌํ•จ

๊ฒฝ๋กœ ๋ณด์•ˆ (Path Traversal ๋ฐฉ์–ด)

ํŒŒ์ผ: app/Rules/SafeTemplatePath.php

G7์€ OS ๋ ˆ๋ฒจ ํŒŒ์ผ ๊ถŒํ•œ์— ์˜์กดํ•˜์ง€ ์•Š๊ณ , ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ ˆ๋ฒจ์—์„œ ๊ฒฝ๋กœ๋ฅผ ์ ๊ทน์ ์œผ๋กœ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.

13๊ฐœ Path Traversal ํŒจํ„ด ์ฐจ๋‹จ

ํŒจํ„ด ์„ค๋ช…
../ 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 ์™ธ๋ถ€ ๊ฒฝ๋กœ ๊ฐ์ง€ ์‹œ ์ฐจ๋‹จ

Hook ๊ธฐ๋ฐ˜ ๊ฒ€์ฆ ํ™•์žฅ

๋ชจ๋“ˆ/ํ”Œ๋Ÿฌ๊ทธ์ธ์€ 6๊ฐœ Filter Hook์„ ํ†ตํ•ด ๋ ˆ์ด์•„์›ƒ ๊ฒ€์ฆ ๊ทœ์น™์„ ๋™์ ์œผ๋กœ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ 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 ๊ตฌ๋ฌธ์€ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ์œผ๋กœ ์•ˆ์ „ ์ฒ˜๋ฆฌ

๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€

ํ…œํ”Œ๋ฆฟ ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ ์ง€์นจ

  1. ์ปดํฌ๋„ŒํŠธ ๋ช…์„ธ ๋ฌธ์„œํ™”: components.json์— ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๋ฅผ basic/composite/layout ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ๋ถ„๋ฅ˜ ๋“ฑ๋ก
  2. ์™ธ๋ถ€ ์˜์กด์„ฑ ์ตœ์†Œํ™”: ์™ธ๋ถ€ CDN ์‚ฌ์šฉ ์ž์ œ, /public/build/template/ ๋””๋ ‰ํ† ๋ฆฌ์— ๋ฒˆ๋“ค๋ง
  3. props ํƒ€์ž… ๊ฒ€์ฆ: ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ TypeScript๋กœ props ํƒ€์ž… ๊ฒ€์ฆ

ํ™•์žฅ ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ ์ง€์นจ

  1. Hook์„ ํ†ตํ•œ ๊ฒ€์ฆ ์ถ”๊ฐ€: ๋ชจ๋“ˆ ์ „์šฉ ๊ฒ€์ฆ์ด ํ•„์š”ํ•˜๋ฉด core.layout.*_validation_rules Hook ์‚ฌ์šฉ
  2. Custom Rule ์ž‘์„ฑ: Illuminate\Contracts\Validation\ValidationRule ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„
  3. __() ํ•จ์ˆ˜ ์‚ฌ์šฉ: Custom Rule ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋Š” ๋ฐ˜๋“œ์‹œ ๋‹ค๊ตญ์–ด ์ฒ˜๋ฆฌ

์‹œ์Šคํ…œ ๊ด€๋ฆฌ์ž๋ฅผ ์œ„ํ•œ ์ง€์นจ

  1. ์ •๊ธฐ์ ์ธ ๋ณด์•ˆ ๊ฐ์‚ฌ: MaliciousJsonTest.php ์‹คํ–‰์œผ๋กœ ๋ฐฉ์–ด ์ฒด๊ณ„ ๊ฒ€์ฆ
  2. ๋กœ๊ทธ ๋ชจ๋‹ˆํ„ฐ๋ง: 422 ์‘๋‹ต ๋นˆ๋„ ๋ชจ๋‹ˆํ„ฐ๋ง, ๋ฐ˜๋ณต์ ์ธ ์•…์˜์  ์‹œ๋„ IP ์ฐจ๋‹จ
  3. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ณดํ˜ธ: .env ํŒŒ์ผ ๊ถŒํ•œ ์ œํ•œ (600), API ํ‚ค/DB ๋น„๋ฐ€๋ฒˆํ˜ธ ์•ˆ์ „ ๋ณด๊ด€
  4. ์—…๋ฐ์ดํŠธ ๊ด€๋ฆฌ: Laravel ๋ฐ ์˜์กด์„ฑ ํŒจํ‚ค์ง€ ์ตœ์‹  ๋ฒ„์ „ ์œ ์ง€

์ฝ”๋“œ ๋ฆฌ๋ทฐ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

  • ๋ชจ๋“  ๊ฒ€์ฆ ๋กœ์ง์ด FormRequest์— ์žˆ๋Š”๊ฐ€?
  • Service์— ๊ฒ€์ฆ ๋กœ์ง์ด ์—†๋Š”๊ฐ€?
  • Custom Rule์—์„œ __() ํ•จ์ˆ˜๋กœ ๋‹ค๊ตญ์–ด ์ฒ˜๋ฆฌํ–ˆ๋Š”๊ฐ€?
  • ์™ธ๋ถ€ URL ์ฐธ์กฐ๊ฐ€ ์—†๋Š”๊ฐ€?
  • dangerouslySetInnerHTML ์‚ฌ์šฉ์ด ์—†๋Š”๊ฐ€?
  • DB ์ฟผ๋ฆฌ๊ฐ€ Eloquent ORM์„ ์‚ฌ์šฉํ•˜๋Š”๊ฐ€?
  • API ๋ผ์šฐํŠธ์— ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ์žˆ๋Š”๊ฐ€?
  • ์ƒˆ๋กœ์šด ์—”๋“œํฌ์ธํŠธ๊ฐ€ WhitelistedEndpoint ํŒจํ„ด์— ๋ถ€ํ•ฉํ•˜๋Š”๊ฐ€?
  • ๋ ˆ์ด์•„์›ƒ ์ƒ์† ์‹œ ๋ถ€๋ชจ ์Šฌ๋กฏ๊ณผ ๋ฐ์ดํ„ฐ์†Œ์Šค ID ์ถฉ๋Œ์ด ์—†๋Š”๊ฐ€?

๊ด€๋ จ ๋ฌธ์„œ

๋ณด์•ˆ ๊ด€๋ จ

๋ฐฑ์—”๋“œ

ํ™•์žฅ ์‹œ์Šคํ…œ

ํ”„๋ก ํŠธ์—”๋“œ

์ฐธ๊ณ  ํŒŒ์ผ

  • 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

There aren't any published security advisories