웹 애플리케이션을 개발할 때, 사용자로부터 입력받은 데이터를 HTML 페이지에 그대로 표시하는 것은 매우 위험합니다. 이는 XSS (Cross-Site Scripting) 공격의 문을 활짝 여는 것과 같습니다. 악의적인 사용자가 <script> 태그를 포함한 데이터를 제출하고, 이 데이터가 다른 사용자의 브라우저에서 그대로 렌더링되면, 세션 쿠키가 탈취되거나 악성 코드가 실행될 수 있습니다.

Django는 이러한 보안 위협을 원천적으로 차단하고, HTML을 안전하게 처리하기 위한 강력한 도구 모음인 django.utils.html을 제공합니다. 🛡️


1. XSS 방어의 핵심: escape()



이 모듈의 가장 기본이자 핵심인 함수입니다. escape()는 문자열 내의 특정 HTML 특수 문자를 HTML 엔티티(Entity)로 변환하여, 브라우저가 이를 태그가 아닌 일반 텍스트로 인식하게 만듭니다.

  • <&lt;
  • >&gt;
  • ' (작은따옴표) 는 &#39;
  • " (큰따옴표) 는 &quot;
  • &&amp;

예제:

from django.utils.html import escape

# 악의적인 사용자 입력
malicious_input = "<script>alert('XSS Attack!');</script>"

# escape 처리
safe_output = escape(malicious_input)

print(safe_output)
# 결과:
# &lt;script&gt;alert(&#39;XSS Attack!&#39;);&lt;/script&gt;

이렇게 변환된 문자열은 브라우저에서 스크립트로 실행되지 않고, <script>alert('XSS Attack!');</script>라는 텍스트가 그대로 화면에 보이게 됩니다.

[중요] Django 템플릿의 자동 이스케이프 (Autoescaping)

다행히도 Django 템플릿 엔진은 기본적으로 모든 변수를 자동으로 escape 처리합니다.

{{ user_input }}

따라서 escape() 함수는 템플릿 외부에서(예: 뷰 로직, API 응답 생성 시) HTML을 수동으로 처리해야 할 때 주로 사용됩니다.


2. 모든 HTML 태그 제거하기: strip_tags()

때로는 HTML을 이스케이프하는 것을 넘어, 아예 모든 태그를 제거하고 순수한 텍스트(plain text)만 추출하고 싶을 때가 있습니다. 예를 들어, 블로그 본문의 HTML 태그를 모두 제거하고 검색 결과 요약문으로 사용하고 싶을 때입니다.

strip_tags()가 바로 이 역할을 합니다.

예제:

from django.utils.html import strip_tags

html_content = "<p>이것은 <strong>매우 중요한</strong> <em>공지사항</em>입니다.</p>"

plain_text = strip_tags(html_content)

print(plain_text)
# 결과:
# 이것은 매우 중요한 공지사항입니다.
# (태그 사이의 공백도 깔끔하게 정리됩니다)

3. 안전하게 HTML 생성하기: format_html()



가장 강력하고 중요한 함수 중 하나입니다.

템플릿이 아닌 파이썬 코드(예: views.pymodels.py)에서 동적으로 HTML을 생성해야 할 때가 있습니다. 예를 들어, 모델의 메서드가 관리자 페이지에서 특정 형식의 링크를 반환하게 하고 싶을 수 있습니다.

이때 파이썬의 f-string이나 + 연산자로 문자열을 조립하면 XSS 공격에 매우 취약해집니다.

format_html(format_string, *args, **kwargs)format_string을 제외한 모든 인자(args, kwargs)를 자동으로 escape() 처리한 후 문자열에 삽입합니다. 그리고 최종 결과를 "이 HTML은 안전하다"고 표시(mark_safe)하여 템플릿에서 이스케이프되지 않고 그대로 렌더링되도록 합니다.

예제: (모델 메서드에서 관리자 페이지용 링크 생성)

from django.db import models
from django.utils.html import format_html
from django.utils.text import slugify

class Post(models.Model):
    title = models.CharField(max_length=100)

    def get_edit_link(self):
        # [나쁜 예] f-string: self.title에 <script>가 있다면 XSS 발생
        # return f'<a href="/admin/blog/post/{self.id}/change/">{self.title}</a>'

        # [좋은 예] format_html 사용
        # self.id와 self.title은 자동으로 escape 처리됩니다.
        url = f"/admin/blog/post/{self.id}/change/"
        return format_html(
            '<a href="{}">{} (수정)</a>',
            url,
            self.title  # 만약 title이 "My<script>..." 라도 "&lt;script&gt;"로 바뀜
        )

4. 텍스트 서식 도우미: linebreaksurlize

이 함수들은 템플릿 필터(|linebreaks, |urlize)의 원본 함수이며, 순수 텍스트를 HTML 서식으로 변환할 때 유용합니다.

  • linebreaks(text) : 일반 텍스트의 줄바꿈 문자(\n)를 HTML의 <p> 태그나 <br> 태그로 변환합니다. 사용자가 textarea에 입력한 텍스트를 서식을 유지한 채 보여줄 때 유용합니다.
  • urlize(text) : 텍스트 내의 http://..., https://..., www... 같은 URL 패턴을 찾아 자동으로 <a> 태그로 감싸줍니다.

예제:

from django.utils.html import linebreaks, urlize

raw_text = """안녕하세요.
django.utils.html을 테스트 중입니다.

방문 사이트: https://www.djangoproject.com
"""

# 1. 줄바꿈 적용
html_with_breaks = linebreaks(raw_text)
# 결과 (대략):
# <p>안녕하세요.<br>django.utils.html을 테스트 중입니다.</p>
# <p>방문 사이트: https://www.djangoproject.com</p>

# 2. URL 링크 적용
html_with_links = urlize(html_with_breaks)
# 결과 (대략):
# ...
# <p>방문 사이트: <a href="https://www.djangoproject.com" rel="nofollow">https://www.djangoproject.com</a></p>

5. 여러 항목을 안전하게 HTML로 결합: format_html_join()

format_html()이 단일 항목을 포매팅한다면, format_html_join()여러 항목(리스트, 튜플 등)을 순회하며 안전하게 HTML로 결합할 때 사용합니다.

format_html_join(separator, format_string, args_list) 형식으로 사용합니다.

  • separator: 각 항목을 구분할 HTML (예: '\n', <br>)
  • format_string: 각 항목에 적용할 HTML 형식 (예: <li>{}</li>)
  • args_list: format_string에 순차적으로 대입될 데이터 리스트

예제: (파이썬 리스트를 <ul> 태그로 변환)

from django.utils.html import format_html_join
from django.utils.safestring import mark_safe

options = [
    ('item1', '항목 1'),
    ('item2', '<strong>위험한 항목 2</strong>'),
]

# format_string에서 {}는 args_list의 각 튜플 전체를 의미합니다.
# {0}은 튜플의 첫 번째 요소, {1}은 두 번째 요소를 의미합니다.
# '항목 1'과 '<strong>...' 부분은 자동으로 escape 됩니다.
list_items = format_html_join(
    '\n',  # 각 항목을 줄바꿈으로 구분
    '<li><input type="radio" value="{0}">{1}</li>', # 각 항목에 적용할 서식
    options  # 데이터 리스트
)

# list_items는 '안전한' HTML 조각이 됩니다.
final_html = format_html('<ul>\n{}\n</ul>', list_items)

# Django 템플릿에서 final_html을 {{ final_html }}로 렌더링하면...

결과 (HTML 소스):

<ul>
<li><input type="radio" value="item1">항목 1</li>
<li><input type="radio" value="item2">&lt;strong&gt;위험한 항목 2&lt;/strong&gt;</li>
</ul>

6. 데이터를 안전하게 / 태그로 전달: json_script()

Django 템플릿에서 파이썬 데이터를 JavaScript 변수로 전달해야 할 때가 많습니다. 이때 json_script(data, element_id)를 사용하면 매우 편리하고 안전합니다.

이 함수는 파이썬 딕셔너리나 리스트를 JSON 문자열로 변환하고, 이를 application/json 타입의 <script> 태그 안에 삽입해 줍니다.

예제: (뷰에서 데이터를 전달)

# views.py
from django.utils.html import json_script

def my_view(request):
    user_data = {
        'id': request.user.id,
        'username': request.user.username,
        'isAdmin': request.user.is_superuser,
    }
    # user_data를 JSON으로 변환하여 <script id="user-data-json"> 안에 넣음
    context = {
        'user_data_json': json_script(user_data, 'user-data-json')
    }
    return render(request, 'my_template.html', context)

템플릿 (my_template.html):

{{ user_data_json }}

<script>
    const dataElement = document.getElementById('user-data-json');
    const userData = JSON.parse(dataElement.textContent);

    console.log(userData.username); // "admin"
</script>

이 방식은 데이터를 수동으로 var user = {{ user_data }};처럼 삽입하려다 "' 문자로 인해 발생하는 구문 오류나 XSS 취약점을 완벽하게 방지합니다.


7. [고급] HTML이 안전함을 명시하기: mark_safe() / html_safe

가끔은 개발자가 의도적으로 HTML을 생성했으며, 이 HTML이 100% 안전하다고 확신하여 Django의 자동 이스케이프(autoescape) 기능을 끄고 싶을 때가 있습니다.

format_html()이나 json_script() 같은 함수들은 내부적으로 이 처리를 자동으로 수행합니다.

  • mark_safe(s): 문자열 s에 "이것은 안전한 HTML이니 이스케이프하지 마세요"라는 '안전 딱지'를 붙여 반환합니다. 이 함수 자체는 아무런 이스케이프 처리를 하지 않습니다. 따라서 신뢰할 수 없는 데이터에 절대 사용해서는 안 됩니다.

  • @html_safe (데코레이터): 모델의 메서드나 커스텀 템플릿 태그 함수가 반환하는 문자열이 안전한 HTML임을 명시할 때 사용합니다. format_html을 사용하기 애매한 복잡한 로직으로 HTML을 생성할 때 유용합니다.

예제: (모델 메서드에 적용)

from django.db import models
from django.utils.html import format_html, html_safe

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField()

    # 이 메서드는 format_html을 사용했으므로 이미 안전함 (권장 방식)
    def get_username_display(self):
        return format_html("<strong>{}</strong>", self.user.username)

    # 이 메서드는 복잡한 로직 후 @html_safe로 안전함을 표시 (고급 방식)
    @html_safe
    def get_complex_display(self):
        # ... (개발자가 안전하다고 보장하는 복잡한 HTML 조합 로직) ...
        html_string = f"<div>{self.user.username}</div><p>{self.bio}</p>"
        # 이 방식은 bio에 <script>가 있다면 XSS에 취약합니다.
        # @html_safe는 매우 신중하게 사용해야 합니다.
        return html_string

요약

django.utils.html 모듈은 Django의 핵심적인 보안 철학(Autoescaping)을 파이썬 코드 레벨에서 구현할 수 있게 해주는 필수 도구입니다.

  • XSS를 막으려면 escape()를 사용합니다. (템플릿에서는 자동)
  • 태그를 모두 지우려면 strip_tags()를 사용합니다.
  • 파이썬 코드에서 안전하게 HTML을 생성하려면 반드시 format_html()을 사용해야 합니다.
  • 리스트 데이터를 HTML로 결합할 때는 format_html_join()을 사용합니다.
  • 파이썬 데이터를 JavaScript로 넘길 때는 json_script()가 가장 안전하고 표준적인 방법입니다.
  • mark_safe@html_safe는 자동 이스케이프를 무력화하므로, 정말로 필요한 경우가 아니라면 format_html을 대신 사용하는 것이 좋습니다.

이 도구들을 올바르게 이해하고 사용하면, 보안이 견고한 Django 애플리케이션을 만들 수 있습니다.