Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 675 – 임의의 리터럴 문자열 타입

Author:
Pradeep Kumar Srinivasan <gohanpra at gmail.com>, Graham Bleaney <gbleaney at gmail.com>
Sponsor:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
Discussions-To:
Typing-SIG thread
Status:
Final
Type:
Standards Track
Topic:
Typing
Created:
30-Nov-2021
Python-Version:
3.11
Post-History:
07-Feb-2022
Resolution:
Python-Dev message

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

Important

This PEP is a historical document: see LiteralString and typing.LiteralString for up-to-date specs and documentation. Canonical typing specs are maintained at the typing specs site; runtime typing behaviour is described in the CPython documentation.

×

See the typing specification update process for how to propose changes to the typing spec.

개요

현재는 타입 어노테이션을 사용하여 함수 매개변수가 임의의 리터럴 문자열 타입일 수 있음을 지정할 방법이 없습니다. Literal["foo"]와 같은 정확한 리터럴 문자열 타입을 지정해야 합니다. 이 PEP는 리터럴 문자열 타입의 상위 타입인 LiteralString을 도입합니다. 이를 통해 함수가 Literal["foo"] 또는 Literal["bar"]와 같은 임의의 리터럴 문자열 타입을 허용할 수 있습니다.

동기

SQL 또는 셸 명령을 실행하는 강력한 API는 임의의 사용자 제어 문자열이 아니라 리터럴 문자열을 사용하여 호출할 것을 권장하는 경우가 많습니다. 그러나 타입 시스템으로는 이러한 권장 사항을 표현할 방법이 없으므로, 개발자가 이를 따르지 않을 때 보안 취약점이 발생하는 경우가 있습니다. 예를 들어, 데이터베이스에서 사용자 레코드를 조회하는 순진한 방법은 사용자 ID를 받아 미리 정의된 SQL 쿼리에 삽입하는 것입니다:

def query_user(conn: Connection, user_id: str) -> User:
    query = f"SELECT * FROM data WHERE user_id = {user_id}"
    conn.execute(query)
    ...  # Transform data to a User object and return it

query_user(conn, "user123")  # OK.

그러나 사용자 제어 데이터인 user_id가 SQL 명령 문자열과 섞이고 있으므로, 악의적인 사용자가 임의의 SQL 명령을 실행할 수 있습니다:

 # Delete the table.
 query_user(conn, "user123; DROP TABLE data;")

 # Fetch all users (since 1 = 1 is always true).
 query_user(conn, "user123 OR 1 = 1")

이러한 SQL 삽입 공격을 방지하기 위해 SQL API는 매개변수화된 쿼리를 제공하며, 이는 실행할 쿼리와 사용자 제어 데이터를 분리하여 임의의 쿼리를 실행할 수 없게 합니다. 예를 들어, sqlite3를 사용하면 원래 함수는 매개변수를 사용하는 쿼리로 안전하게 작성됩니다:

def query_user(conn: Connection, user_id: str) -> User:
    query = "SELECT * FROM data WHERE user_id = ?"
    conn.execute(query, (user_id,))
    ...

문제는 이러한 규율을 강제할 방법이 없다는 것입니다. sqlite3 자체의 documentation은 외부 입력으로부터 sql 인자를 동적으로 작성하지 말라고 독자에게 경고할 수 있을 뿐이며, API 작성자는 이를 타입 시스템을 통해 표현할 수 없습니다. 사용자는 이전처럼 편리한 f-string을 계속 사용할 수 있고 실제로 자주 그렇게 하므로, 코드가 SQL 삽입에 취약한 상태로 남을 수 있습니다.

널리 사용되는 보안 린터인 Bandit와 같은 기존 도구는 AST를 검사하거나 다른 의미론적 패턴 매칭을 사용하여 SQL API에 사용된 안전하지 않은 외부 데이터를 탐지하려고 합니다. 그러나 이러한 도구는 실행 전에 긴 여러 줄 쿼리를 변수에 저장하거나, 특정 조건에 따라 쿼리에 리터럴 문자열 수정자를 추가하거나, 함수를 사용하여 쿼리 문자열을 변환하는 것과 같은 일반적인 관용구를 허용하지 않습니다. (기존 도구에 대해서는 Rejected Alternatives 절에서 조사합니다.) 예를 들어 많은 도구는 다음의 무해한 코드 조각에서 오탐 문제를 탐지합니다:

def query_data(conn: Connection, user_id: str, limit: bool) -> None:
    query = """
        SELECT
            user.name,
            user.age
        FROM data
        WHERE user_id = ?
    """
    if limit:
        query += " LIMIT 1"

    conn.execute(query, (user_id,))

위와 같은 무해한 관용구는 허용하면서 사용자 제어 데이터의 유해한 실행은 금지하고, 사용자에게 추가 작업을 요구하지 않기를 원합니다.

이러한 목표를 달성하기 위해 리터럴로 구성된 것으로 알려진 문자열 값만 허용하는 LiteralString 타입을 도입합니다. 이는 PEP 586Literal["foo"] 타입을 일반화한 것입니다. LiteralString 타입의 문자열에는 사용자 제어 데이터가 포함될 수 없습니다. 따라서 LiteralString만 허용하는 모든 API는 삽입 취약점의 영향을 받지 않습니다(pragmatic limitations이 있음).

사용자 입력으로 작성된 문자열을 sqlite3execute 메서드가 허용하지 않도록 하려면, 해당 typeshed stub에서 LiteralString 타입인 sql 쿼리를 허용하도록 작성합니다:

from typing import LiteralString

def execute(self, sql: LiteralString, parameters: Iterable[str] = ...) -> Cursor: ...

이렇게 하면 안전하지 않은 SQL 예제가 성공적으로 금지됩니다. 아래의 query 변수는 user_id를 사용한 포맷 문자열로 생성되므로 str 타입으로 추론되며, execute에 전달할 수 없습니다:

def query_user(conn: Connection, user_id: str) -> User:
    query = f"SELECT * FROM data WHERE user_id = {user_id}"
    conn.execute(query)  # Error: Expected LiteralString, got str.
    ...

이 메서드는 더 복잡한 예제를 허용할 만큼 충분히 유연합니다:

def query_data(conn: Connection, user_id: str, limit: bool) -> None:
    # This is a literal string.
    query = """
        SELECT
            user.name,
            user.age
        FROM data
        WHERE user_id = ?
    """

    if limit:
        # Still has type LiteralString because we added a literal string.
        query += " LIMIT 1"

    conn.execute(query, (user_id,))  # OK

사용자는 SQL 코드를 전혀 변경할 필요가 없었다는 점에 주목하십시오. 타입 검사기는 리터럴 문자열 타입을 추론하고 위반이 발생한 경우에만 경고할 수 있었습니다.

LiteralString은 셸 명령을 작성하거나 이스케이프하지 않고 문자열을 HTML 응답에 삽입하는 경우처럼 명령과 데이터를 엄격히 분리하려는 다른 상황에서도 유용합니다(Appendix A: Other Uses 참조). 전반적으로 이러한 엄격성과 유연성의 조합을 통해 사용자를 부담스럽게 하지 않으면서 민감한 코드에서 더 안전한 API 사용을 쉽게 강제할 수 있습니다.

사용 통계

sqlite3를 사용하는 오픈 소스 프로젝트를 표본 조사한 결과, conn.execute~67% of the time에는 안전한 문자열 리터럴과 함께 호출되었고, ~33% of the time에는 잠재적으로 안전하지 않은 로컬 문자열 변수와 함께 호출되었다는 사실을 발견했습니다. 이 PEP의 리터럴 문자열 유형을 타입 검사기와 함께 사용하면 해당 33% 사례 중 안전하지 않은 부분, 즉 사용자가 제어하는 데이터가 쿼리에 포함되는 경우를 방지하면서도 안전한 사례는 원활하게 그대로 사용할 수 있습니다.

근거

먼저, 보안 취약점을 방지하는 데 types를 사용하는 이유는 무엇입니까?

문서에서 사용자에게 경고하는 것만으로는 충분하지 않습니다. 대부분의 사용자는 이러한 경고를 전혀 보지 못하거나 무시합니다. 기존의 동적 또는 정적 분석 접근법을 사용하면 지나치게 제한적입니다. 이러한 접근법은 Motivation 섹션에서 살펴본 것처럼 자연스러운 관용구를 허용하지 않으며, Rejected Alternatives 섹션에서 더 자세히 논의할 예정입니다. 이 PEP의 타입 기반 접근법은 엄격성과 유연성 사이에서 사용자 친화적인 균형을 이룹니다.

런타임 접근법이 작동하지 않는 이유는 런타임에서 쿼리 문자열이 일반적인 str이기 때문입니다. 명백히 악의적인 페이로드를 정규식으로 필터링하는 등의 휴리스틱을 사용하여 일부 공격을 방지할 수는 있지만, 이를 우회할 방법은 항상 존재합니다. (정상적인 쿼리와 악성 쿼리를 완벽하게 구별하는 문제는 정지 문제로 귀결됩니다.)

쿼리 문자열이 리터럴 문자열 표현식인지 확인하기 위해 AST를 검사하는 등의 정적 접근법으로는 문자열이 중간 변수에 할당된 경우나 무해한 함수에 의해 변환된 경우를 판별할 수 없습니다. 이로 인해 이러한 접근법은 지나치게 제한적입니다.

타입 검사기는 런타임 또는 정적 분석 접근법에서는 사용할 수 없는 정보에 접근할 수 있으므로, 놀랍게도 두 접근법보다 더 나은 결과를 냅니다. 구체적으로 타입 검사기는 표현식에 리터럴 문자열 유형, 예를 들어 Literal["foo"]가 있는지 알려줄 수 있습니다. 타입 검사기는 이미 변수 할당이나 함수 호출을 거쳐 유형을 전파합니다.

현재 타입 시스템 자체에서 SQL 또는 셸 명령 실행 함수가 가능한 입력 문자열 세 개만 허용한다면 작업은 끝났을 것입니다. 다음과 같이 말하면 됩니다.

def execute(query: Literal["foo", "bar", "baz"]) -> None: ...

하지만 물론 execute는 가능한 모든 쿼리를 받아들일 수 있습니다. 쿼리에 임의의 사용자 제어 문자열이 포함되지 않도록 하려면 어떻게 해야 합니까?

값이 Literal[<...>]유형이어야 하며, 여기서 <...>는 어떤 문자열이라는 점을 지정하고자 합니다. 이것이 LiteralString이 나타내는 것입니다. LiteralString은 모든 리터럴 문자열 유형의 “상위 타입”입니다. 사실상 이 PEP는 Literal["foo"]str사이의 타입 계층에 유형 하나를 도입할 뿐입니다. Literal["foo"]또는 Literal["bar"]와 같은 특정 리터럴 문자열은 LiteralString과 호환되지만, 그 역은 성립하지 않습니다. LiteralString자체의 “상위 타입”은 str입니다. 따라서 LiteralStringstr과 호환되지만, 그 역은 성립하지 않습니다.

리터럴 유형의 UnionUnion의 각 요소가 개별적으로 LiteralString과 호환되므로 자연스럽게 LiteralString과 호환됩니다. 따라서 Literal["foo", "bar"]LiteralString과 호환됩니다.

그러나 정확한 리터럴 쿼리만 표현하려는 것은 아니라는 점을 기억하시기 바랍니다. 또한 query + " LIMIT 1"과 같이 두 리터럴 문자열의 조합도 지원하려고 합니다. 이 역시 앞서 설명한 개념으로 가능합니다. xyLiteralString유형의 두 값이라면 x + y역시 LiteralString과 호환되는 유형이 됩니다. Literal["foo"]Literal["bar"]와 같은 구체적인 인스턴스를 살펴보면 이를 추론할 수 있습니다. 더해진 문자열 x + y의 값은 "foobar"일 수밖에 없고, 이는 Literal["foobar"]유형을 가지므로 LiteralString과 호환됩니다. xy가 리터럴 유형의 유니언인 경우에도 같은 추론이 적용됩니다. 각각 xy에서 임의의 두 리터럴 유형을 쌍별로 더한 결과는 리터럴 유형이며, 이는 전체 결과가 리터럴 유형의 Union이고 따라서 LiteralString과 호환된다는 것을 의미합니다.

이러한 방식으로 Python의 Literal 문자열 타입 개념을 활용하여, API가 리터럴로 구성된 것으로 알려진 문자열만 허용하도록 지정할 수 있습니다. 더 구체적인 세부 사항은 나머지 섹션에서 설명합니다.

사양

런타임 동작

typing.NoReturn과 유사하게 구현한 LiteralStringtyping.py에 추가할 것을 제안합니다.

LiteralString은 타입 검사만을 위해 사용되는 특수 형식이라는 점에 유의하십시오. 런타임에 type(<expr>)LiteralString을 생성하는 표현식은 없습니다. 따라서 구현에서 이것이 str의 서브클래스라고 지정하지 않습니다.

LiteralString의 유효한 위치

LiteralString은 다른 모든 타입을 사용할 수 있는 곳에서 사용할 수 있습니다.

variable_annotation: LiteralString

def my_function(literal_string: LiteralString) -> LiteralString: ...

class Foo:
    my_attribute: LiteralString

type_argument: List[LiteralString]

T = TypeVar("T", bound=LiteralString)

Literal 타입의 유니언 내부에 중첩할 수는 없습니다.

bad_union: Literal["hello", LiteralString]  # Not OK
bad_nesting: Literal[LiteralString]  # Not OK

타입 추론

LiteralString 추론

모든 리터럴 문자열 타입은 LiteralString과 호환됩니다. 예를 들어 x: LiteralString = "foo"가 유효한 이유는 "foo"Literal["foo"] 타입으로 추론되기 때문입니다.

Rationale에 설명된 대로, 다음과 같은 경우에도 LiteralString으로 추론합니다.

  • 덧셈: xy가 모두 LiteralString과 호환되면 x + y의 타입은 LiteralString입니다.
  • 결합: sep의 타입이 LiteralString과 호환되고 xs의 타입이 Iterable[LiteralString]과 호환되면 sep.join(xs)의 타입은 LiteralString입니다.
  • 제자리 덧셈: s의 타입이 LiteralString이고 x의 타입이 LiteralString과 호환되면 s += xs의 타입을 LiteralString으로 유지합니다.
  • 문자열 형식 지정: f-string을 구성하는 표현식이 리터럴 문자열인 경우에만 f-string의 타입은 LiteralString입니다. s와 인자의 타입이 LiteralString과 호환되는 경우에만 s.format(...)의 타입은 LiteralString입니다.
  • 리터럴 보존 메서드: Appendix CLiteralString 타입을 보존하는 str 메서드의 전체 목록을 제공합니다.

그 밖의 모든 경우에는 구성된 값 중 하나 이상이 리터럴이 아닌 str 타입이면 타입을 구성한 결과의 타입은 str이 됩니다. 예를 들어 s의 타입이 str이면 "hello" + s의 타입은 str입니다. 이는 타입 검사기의 기존 동작과 일치합니다.

LiteralStringstr 타입과 호환됩니다. str의 모든 메서드를 상속합니다. 따라서 LiteralString 타입의 변수 s가 있다면 s.startswith("hello")를 작성해도 안전합니다.

일부 타입 검사기는 동등성 검사를 수행할 때 문자열의 타입을 더 구체적인 타입으로 좁힙니다.

def foo(s: str) -> None:
    if s == "bar":
        reveal_type(s)  # => Literal["bar"]

if 블록의 이러한 구체화된 타입도 그 타입이 Literal["bar"]이므로 LiteralString과 호환됩니다.

예제

위 규칙을 명확히 이해하는 데 도움이 되도록 다음 예제를 참조하십시오.

literal_string: LiteralString
s: str = literal_string  # OK

literal_string: LiteralString = s  # Error: Expected LiteralString, got str.
literal_string: LiteralString = "hello"  # OK

리터럴 문자열의 덧셈:

def expect_literal_string(s: LiteralString) -> None: ...

expect_literal_string("foo" + "bar")  # OK
expect_literal_string(literal_string + "bar")  # OK

literal_string2: LiteralString
expect_literal_string(literal_string + literal_string2)  # OK

plain_string: str
expect_literal_string(literal_string + plain_string)  # Not OK.

리터럴 문자열을 사용한 결합:

expect_literal_string(",".join(["foo", "bar"]))  # OK
expect_literal_string(literal_string.join(["foo", "bar"]))  # OK
expect_literal_string(literal_string.join([literal_string, literal_string2]))  # OK

xs: List[LiteralString]
expect_literal_string(literal_string.join(xs)) # OK
expect_literal_string(plain_string.join([literal_string, literal_string2]))
# Not OK because the separator has type 'str'.

리터럴 문자열을 사용한 제자리 덧셈:

literal_string += "foo"  # OK
literal_string += literal_string2  # OK
literal_string += plain_string # Not OK

리터럴 문자열을 사용한 포맷 문자열:

literal_name: LiteralString
expect_literal_string(f"hello {literal_name}")
# OK because it is composed from literal strings.

expect_literal_string("hello {}".format(literal_name))  # OK

expect_literal_string(f"hello")  # OK

username: str
expect_literal_string(f"hello {username}")
# NOT OK. The format-string is constructed from 'username',
# which has type 'str'.

expect_literal_string("hello {}".format(username))  # Not OK

리터럴 정수와 같은 다른 리터럴 형식은 LiteralString과 호환되지 않습니다:

some_int: int
expect_literal_string(some_int)  # Error: Expected LiteralString, got int.

literal_one: Literal[1] = 1
expect_literal_string(literal_one)  # Error: Expected LiteralString, got Literal[1].

리터럴 문자열에서 함수를 호출할 수 있습니다:

def add_limit(query: LiteralString) -> LiteralString:
    return query + " LIMIT = 1"

def my_query(query: LiteralString, user_id: str) -> None:
    sql_connection().execute(add_limit(query), (user_id,))  # OK

조건문과 조건식은 예상대로 작동합니다:

def return_literal_string() -> LiteralString:
    return "foo" if condition1() else "bar"  # OK

def return_literal_str2(literal_string: LiteralString) -> LiteralString:
    return "foo" if condition1() else literal_string  # OK

def return_literal_str3() -> LiteralString:
    if condition1():
        result: Literal["foo"] = "foo"
    else:
        result: LiteralString = "bar"

    return result  # OK

TypeVars 및 Generics와의 상호 작용

TypeVars를 LiteralString에 바인딩할 수 있습니다:

from typing import Literal, LiteralString, TypeVar

TLiteral = TypeVar("TLiteral", bound=LiteralString)

def literal_identity(s: TLiteral) -> TLiteral:
    return s

hello: Literal["hello"] = "hello"
y = literal_identity(hello)
reveal_type(y)  # => Literal["hello"]

s: LiteralString
y2 = literal_identity(s)
reveal_type(y2)  # => LiteralString

s_error: str
literal_identity(s_error)
# Error: Expected TLiteral (bound to LiteralString), got str.

LiteralString을 제네릭 클래스의 타입 인자로 사용할 수 있습니다:

class Container(Generic[T]):
    def __init__(self, value: T) -> None:
        self.value = value

literal_string: LiteralString = "hello"
x: Container[LiteralString] = Container(literal_string)  # OK

s: str
x_error: Container[LiteralString] = Container(s)  # Not OK

List와 같은 표준 컨테이너는 예상대로 작동합니다:

xs: List[LiteralString] = ["foo", "bar", "baz"]

오버로드와의 상호 작용

리터럴 문자열과 오버로드는 특별한 방식으로 상호 작용할 필요가 없습니다. 기존 규칙이 정상적으로 작동합니다. 특정 Literal["foo"] 타입이 일치하지 않는 경우 LiteralString을 대체 오버로드로 사용할 수 있습니다:

@overload
def foo(x: Literal["foo"]) -> int: ...
@overload
def foo(x: LiteralString) -> bool: ...
@overload
def foo(x: str) -> str: ...

x1: int = foo("foo")  # First overload.
x2: bool = foo("bar")  # Second overload.
s: str
x3: str = foo(s)  # Third overload.

하위 호환성

이전 Python 버전에서 사용할 수 있도록 typing_extensions.LiteralString을 추가할 것을 제안합니다.

관련 PEP 586 mentions에서 언급하듯이, 타입 검사기는 “더 정교한 추론 기법을 자유롭게 실험할 수 있어야 합니다”. 따라서 리터럴 문자열로 초기화된 타입 주석이 없는 변수에 대해 타입 검사기가 리터럴 문자열 타입을 추론한다면 다음 예제는 정상이어야 합니다:

x = "hello"
expect_literal_string(x)
# OK, because x is inferred to have type 'Literal["hello"]'.

이를 통해 코드에 전혀 주석을 추가하지 않고도 관용적인 SQL 쿼리 코드의 타입을 정확하게 검사할 수 있습니다(Motivation 섹션의 예제에서 볼 수 있듯이).

그러나 PEP 586과 마찬가지로 이 PEP는 위의 추론 전략을 의무화하지 않습니다. 타입 검사기가 x의 타입을 Literal["hello"]로 추론하지 않는 경우 사용자는 x: LiteralString으로 명시적으로 주석을 추가하여 타입 검사기를 보조할 수 있습니다:

x: LiteralString = "hello"
expect_literal_string(x)

거부된 대안

도구 X를 사용하지 않는 이유는 무엇입니까?

SQL 인젝션과 같은 문제를 포착하는 도구는 AST 기반, 함수 수준 분석, 오염 흐름 분석의 세 가지 유형으로 나뉘는 것으로 보입니다:

AST 기반 도구: Bandit에는 SQL 쿼리가 리터럴 문자열이 아닐 때 경고하는 플러그인이 있습니다. 문제는 Motivation 섹션에 나온 것처럼 완전히 안전한 SQL 쿼리 중 상당수가 문자열 리터럴로부터 동적으로 구성된다는 점입니다. AST 수준에서는 결과 SQL 쿼리가 더 이상 문자열 리터럴로 나타나지 않으므로 잠재적으로 악의적인 문자열과 구별할 수 없습니다. 이러한 도구를 사용하려면 개발자가 SQL 쿼리를 작성할 수 있는 능력을 크게 제한해야 합니다. LiteralString은 더 적은 제약으로 유사한 안전성 보장을 제공할 수 있습니다.

Semgrep 및 pyanalyze: Semgrep은 함수 내 constant propagation을 포함하여 더 정교한 함수 수준 분석을 지원합니다. 이를 통해 함수 내에서 일부 형태의 안전한 동적 SQL 쿼리를 허용하면서 인젝션 공격을 방지할 수 있습니다. pyanalyze에도 유사한 확장이 있습니다. 그러나 둘 다 안전한 SQL 쿼리를 구성하고 반환하는 함수 호출은 처리하지 못합니다. 예를 들어 아래 코드 샘플에서 build_insert_query는 해당 열에 여러 값을 삽입하는 쿼리를 생성하는 도우미 함수입니다. Semgrep과 pyanalyze는 이러한 자연스러운 사용을 금지하지만 LiteralString은 프로그래머에게 부담을 주지 않고 이를 처리합니다:

def build_insert_query(
    table: LiteralString
    insert_columns: Iterable[LiteralString],
) -> LiteralString:
    sql = "INSERT INTO " + table

    column_clause = ", ".join(insert_columns)
    value_clause = ", ".join(["?"] * len(insert_columns))

    sql += f" ({column_clause}) VALUES ({value_clause})"
    return sql

def insert_data(
    conn: Connection,
    kvs_to_insert: Dict[LiteralString, str]
) -> None:
    query = build_insert_query("data", kvs_to_insert.keys())
    conn.execute(query, kvs_to_insert.values())

# Example usage
data_to_insert = {
    "column_1": value_1,  # Note: values are not literals
    "column_2": value_2,
    "column_3": value_3,
}
insert_data(conn, data_to_insert)

오염 흐름 분석: Pysa 또는 CodeQL과 같은 도구는 사용자가 제어하는 입력에서 SQL 쿼리로 흐르는 데이터를 추적할 수 있습니다. 이러한 도구는 강력하지만 CI에서 도구를 설정하고, “오염” 싱크와 소스를 정의하며, 개발자에게 사용 방법을 교육하는 데 상당한 오버헤드가 발생합니다. 또한 일반적으로 타입 검사기보다 실행 시간이 더 오래 걸리므로(초 단위가 아니라 분 단위), 피드백이 즉시 제공되지 않습니다. 마지막으로 이러한 도구는 라이브러리 자체가 API를 정확히 어떻게 호출해야 하는지 지정하도록 허용하는 대신(이는 LiteralString로 가능한 방식입니다), 취약점 방지의 부담을 라이브러리 사용자에게 전가합니다.

전용 도구보다 새 타입을 사용하는 것이 더 나은 마지막 이유는 타입 검사기가 전용 보안 도구보다 널리 사용되기 때문입니다. 예를 들어 MyPy는 2022년 1월에 over 7 million times 다운로드된 반면, Bandit은 less than 2 million times 다운로드되었습니다. 타입 검사기에 보안 보호 기능을 직접 내장하면 더 많은 개발자가 이러한 기능의 혜택을 받게 됩니다.

strNewType을 사용하지 않는 이유는 무엇입니까?

LiteralString이 적합한 모든 API는 대신 NewType("SafeSQL", str)와 같이 Python 타입 시스템 내에서 생성된 다른 타입을 허용하도록 업데이트할 수 있습니다.

SafeSQL = NewType("SafeSQL", str)

def execute(self, sql: SafeSQL, parameters: Iterable[str] = ...) -> Cursor: ...

execute(SafeSQL("SELECT * FROM data WHERE user_id = ?"), user_id)  # OK

user_query: str
execute(user_query)  # Error: Expected SafeSQL, got str.

API를 호출하려면 새 타입을 만들어야 한다는 점이 일부 개발자에게 잠시 멈추어 더 주의를 기울이게 할 수 있지만, 개발자가 사용자 제어 문자열을 새 타입으로 변환한 후 수정된 API에 전달하지 않을 것이라는 보장은 없습니다.

query = f"SELECT * FROM data WHERE user_id = f{user_id}"
execute(SafeSQL(query))  # No error!

SafeSQL에 임의의 입력이 들어오는 것을 방지하는 문제는 다시 원점으로 돌아옵니다. 이는 이론적인 우려만도 아닙니다. Django는 SafeStringmark_safe와 함께 위의 방식을 사용합니다. CVE-2020-13596와 같은 문제는 이 기법이 어떻게 fail할 수 있는지 보여 줍니다.

또한 이 방식은 소스 코드에 침습적인 변경(쿼리를 SafeSQL로 감싸는 작업)이 필요하지만, LiteralString에는 이러한 변경이 필요하지 않다는 점에 유의하십시오. 사용자가 민감한 API에 리터럴 문자열을 전달하기만 하면 이 기능을 인식하지 못한 채 사용할 수 있습니다.

Trusted Types를 모방해 보지 않는 이유는 무엇입니까?

Trusted Types는 DOM 기반 크로스 사이트 스크립팅(XSS)을 방지하기 위한 W3C 사양입니다. XSS는 위험한 브라우저 API가 가공되지 않은 사용자 제어 문자열을 허용할 때 발생합니다. 이 사양은 지정된 살균 함수가 반환하는 “Trusted Types”만 허용하도록 이러한 API를 수정합니다. 이러한 살균 함수는 잠재적으로 악의적인 문자열을 입력으로 받아 이를 검증하거나 어떤 방식으로든 무해하게 만들어야 합니다. 예를 들어 유효한 URL인지 확인하거나 HTML 인코딩을 수행해야 합니다.

Trusted Types의 개념을 Python으로 이식하면 이 문제를 해결할 수 있다고 생각하기 쉽습니다. 그러나 근본적인 차이점은 Trusted Types 살균기의 출력이 일반적으로 실행 가능한 코드가 아니도록 의도된다는 점입니다. 따라서 입력을 HTML 인코딩하거나, 위험한 태그를 제거하거나, 그 밖의 방식으로 입력을 무해하게 만드는 작업은 쉽습니다. SQL 쿼리나 셸 명령에서는 최종 결과가 여전히 실행 가능한 코드여야 합니다. 입력 문자열의 어느 부분이 무해하고 어느 부분이 잠재적으로 악의적인지 안정적으로 판별할 수 있는 살균기를 작성할 방법은 없습니다.

런타임 검사 가능한 LiteralString

LiteralString개념을 정적 타입 검사를 넘어 str객체의 런타임 검사 가능한 속성으로 확장할 수 있습니다. 이를 통해 프레임워크가 동적 문자열에 대해 오류를 발생시키도록 허용하는 등 몇 가지 이점을 얻을 수 있습니다. 이러한 런타임 오류는 타입 오류보다 더 강력한 방어 메커니즘이 됩니다. 타입 오류는 작성자가 타입 검사기를 사용하지 않으면 억제되거나 무시되거나 아예 발견되지 않을 수 있기 때문입니다.

LiteralString 개념을 이렇게 확장하면 Python의 가장 근본적인 타입 중 하나를 변경해야 하므로 제안의 범위가 크게 확대됩니다. Perl의 taint와 유사한 문자열의 런타임 오염 검사는 과거에 considered되고 attempted되었으며 앞으로 다른 사람들이 고려할 수도 있지만, 이러한 확장은 이 PEP의 범위를 벗어납니다.

거부된 이름

리터럴 문자열 타입에 사용할 다양한 이름을 검토하고 typing-sig에서 의견을 요청했습니다. 주목할 만한 대안은 다음과 같습니다:

  • Literal[str]: 이는 Literal["foo"] 타입 이름의 자연스러운 확장이지만, typing-sig에서는 사용자가 이를 str 클래스의 리터럴 타입으로 오해할 수 있다고 반대했습니다.
  • LiteralStr: 이는 LiteralString보다 짧지만 PEP 작성자들에게는 이상하게 보입니다.
  • LiteralDerivedString: 이는 MadeFromLiteralString과 함께 이 타입의 기술적 의미를 가장 잘 포착합니다. 이는 "foo"와 같은 리터럴 표현식의 타입뿐만 아니라, "foo" + "bar"와 같이 리터럴로 구성된 표현식의 타입도 나타냅니다. 그러나 두 이름 모두 장황해 보입니다.
  • StringLiteral: 사용자는 문자열이 소스 코드에 구문 토큰으로 존재하는 기존 개념인 “string literals”과 이를 혼동할 수 있지만, 우리가 말하는 개념은 더 일반적입니다.
  • SafeString: 이는 우리가 의도한 의미에 가깝지만, HTML 태그나 셸 관련 특수 문자를 이스케이프하는 등의 방식으로 문자열이 정제되었다고 사용자를 오도할 수 있습니다.
  • ConstantStr: 이는 리터럴 문자열을 구성한다는 개념을 포착하지 못합니다.
  • StaticStr: 이는 문자열을 정적으로, 즉 프로그램을 실행하지 않고 계산할 수 있다는 의미를 내포하지만 사실이 아닙니다. Motivation 예제에서 볼 수 있듯이 리터럴 문자열은 런타임 플래그에 따라 달라질 수 있습니다.
  • LiteralOnly[str]: 이는 bytes 또는 int와 같은 다른 리터럴 타입으로 확장할 수 있다는 장점이 있습니다. 그러나 가독성 저하를 감수할 만큼 확장성이 가치 있다고 판단하지는 않았습니다.

전반적으로 오랜 기간 typing-sig에서 뚜렷한 승자는 없었으므로, LiteralString을 선호하는 쪽으로 결정을 기울였습니다.

LiteralBytes

Literal[b"foo"]와 같은 리터럴 바이트 타입을 LiteralBytes로 일반화할 수 있습니다. 그러나 리터럴 바이트 타입은 리터럴 문자열 타입보다 훨씬 덜 빈번하게 사용되며 LiteralBytes에 대한 사용자 수요도 많지 않았으므로, 이 PEP에는 포함하지 않기로 결정했습니다. 그러나 다른 사람들이 향후 PEP에서 이를 고려할 수도 있습니다.

참조 구현

이는 Pyre v0.9.8에 구현되어 있으며 활발히 사용되고 있습니다.

구현은 단순히 타입 검사기에 LiteralString을 리터럴 문자열 타입의 상위 타입으로 추가합니다.

덧셈, join 등을 통한 구성을 지원하기 위해 Pyre의 typeshed 사본에서 str의 스텁을 오버로드하는 것으로 충분했습니다.

부록 A: 기타 용도

논의를 단순화하고 최소한의 보안 지식만 요구하기 위해 PEP 전체에서 SQL 인젝션에 초점을 맞췄습니다. 그러나 LiteralString은 다양한 다른 인젝션 취약점도 방지하는 데 사용할 수 있습니다.

명령 인젝션

subprocess.run과 같은 API는 셸 명령으로 실행할 수 있는 문자열을 허용합니다:

subprocess.run(f"echo 'Hello {name}'", shell=True)

사용자가 제어하는 데이터가 명령 문자열에 포함되면 해당 코드는 “command injection”에 취약합니다. 즉, 공격자가 악성 명령을 실행할 수 있습니다. 예를 들어 ' && rm -rf / # 값은 다음과 같은 파괴적인 명령을 실행하게 합니다:

echo 'Hello ' && rm -rf / #'

shell=True 모드에서 사용할 때 runLiteralString만 허용하도록 업데이트하면 이 취약점을 방지할 수 있습니다. 다음은 단순화한 스텁입니다:

def run(command: LiteralString, *args: str, shell: bool=...): ...

사이트 간 스크립팅(XSS)

Django와 같은 가장 널리 사용되는 Python 웹 프레임워크는 사용자 데이터에서 HTML을 생성하기 위해 템플릿 엔진을 사용합니다. 이러한 템플릿 언어는 사용자 데이터를 HTML 템플릿에 삽입하기 전에 자동으로 이스케이프하므로 교차 사이트 스크립팅(XSS) 취약점을 방지합니다.

그러나 HTML을 있는 그대로 렌더링하기 위해 자동 이스케이프 우회 를 수행하는 일반적인 방법은 XSS 취약점을 일으키는 Djangomark_safeJinja2do_mark_safe와 같은 함수를 사용하는 것입니다:

 dangerous_string = django.utils.safestring.mark_safe(f"<script>{user_input}</script>")
 return(dangerous_string)

mark_safeLiteralString만 허용하도록 업데이트하면 이 취약점을 방지할 수 있습니다:

 def mark_safe(s: LiteralString) -> str: ...

서버 측 템플릿 주입(SSTI)

Jinja와 같은 템플릿 프레임워크는 평가된 후 렌더링된 결과에 삽입되는 Python 표현식을 허용합니다:

template_str = "There are {{ len(values) }} values: {{ values }}"
template = jinja2.Template(template_str)
template.render(values=[1, 2])
# Result: "There are 2 values: [1, 2]"

공격자가 템플릿 문자열의 전체 또는 일부를 제어할 수 있다면 임의의 코드를 실행하고 애플리케이션을 침해할 수 있는 표현식을 삽입할 수 있습니다:

 malicious_str = "{{''.__class__.__base__.__subclasses__()[408]('rm - rf /',shell=True)}}"
 template = jinja2.Template(malicious_str)
 template.render()
 # Result: The shell command 'rm - rf /' is run

TemplateAPI가 LiteralString만 허용하도록 업데이트하면 이와 같은 템플릿 주입 공격을 방지할 수 있습니다:

 class Template:
     def __init__(self, source: LiteralString): ...

로깅 형식 문자열 주입

로깅 프레임워크는 입력 문자열에 형식 지정 지시문을 포함할 수 있도록 허용하는 경우가 많습니다. 최악의 경우 사용자가 기록되는 문자열을 제어하도록 허용하면 CVE-2021-44228 이 발생할 수 있으며, 이는 일반적으로 log4shell로 알려져 있고 “지난 10년간 가장 치명적인 취약점” 으로 묘사되었습니다. 현재 유사한 공격에 취약한 것으로 알려진 Python 프레임워크는 없지만, 내장 로깅 프레임워크는 외부에서 제어되는 로깅 문자열을 통한 서비스 거부 공격에 취약한 형식 지정 옵션을 제공합니다. 다음 예제는 간단한 서비스 거부 시나리오를 보여 줍니다:

 external_string = "%(foo)999999999s"
 ...
 # Tries to add > 1GB of whitespace to the logged string:
 logger.info(f'Received: {external_string}', some_dict)

로거에 전달되는 형식 문자열이 LiteralString이어야 하고 외부에서 제어되는 모든 데이터는 인자로 별도로 전달되도록 요구하면 이러한 공격을 방지할 수 있습니다(Issue 46200 에서 제안된 방식):

 def info(msg: LiteralString, *args: object) -> None:
     ...

부록 B: 제한 사항

LiteralString을 사용하더라도 사용자가 리터럴이 아닌 데이터로 구성된 문자열을 API에 전달하는 것을 방지하지 못할 수 있는 방법은 여러 가지가 있습니다:

1. 개발자가 타입 검사기를 사용하지 않거나 타입 어노테이션을 추가하지 않으면 위반 사항이 발견되지 않습니다.

2. cast(LiteralString, non_literal_string)는 타입 검사기에게 거짓말하는 데 사용될 수 있습니다. 타입 검사기를 속여 동적 문자열 값이 LiteralString인 것처럼 가장하게 만들 수 있습니다. 타입이 Any인 변수도 마찬가지입니다.

3. # type: ignore와 같은 주석을 사용하여 리터럴이 아닌 문자열에 대한 경고를 무시할 수 있습니다.

4. str을 다음과 같이 변환하는 간단한 함수를 만들 수 있습니다: LiteralString:

 def make_literal(s: str) -> LiteralString:
     letters: Dict[str, LiteralString] = {
         "A": "A",
         "B": "B",
         ...
     }
     output: List[LiteralString] = [letters[c] for c in s]
     return "".join(output)

린팅, 코드 검토 등을 사용하여 위 문제를 완화할 수 있지만, 궁극적으로 LiteralString이 제공하는 보호를 우회하려는 영리한 악의적 개발자는 항상 성공합니다. 기억해야 할 중요한 점은 LiteralString악의적인 개발자로부터 보호하기 위한 것이 아니라는 점입니다. 이는 선의의 개발자가 민감한 API를 위험한 방식으로 실수로 사용하는 것을 방지하기 위한 것이며, 그 밖의 작업을 방해하지 않습니다.

LiteralString이 없다면 API 작성자가 사용할 수 있는 최선의 강제 수단은 문서화이며, 문서는 쉽게 무시되고 자주 읽히지 않습니다. LiteralString이 있으면 API를 오용하기 위해 의식적인 판단이 필요하고, 검토자와 향후 개발자가 알아차릴 수 있는 코드상의 흔적도 남습니다.

부록 C: LiteralString을 보존하는 str 메서드

str 클래스에는 LiteralString을 활용하면 이점이 있는 메서드가 여러 개 있습니다. 예를 들어 사용자는 Inferring LiteralString 섹션에서 살펴본 다른 예제와 마찬가지로 "hello".capitalize()의 타입이 LiteralString이기를 기대할 수 있습니다. LiteralString타입을 추론하는 것이 올바른 이유는 이 문자열이 임의의 사용자가 제공한 문자열이 아니기 때문입니다. 즉, 이 문자열의 타입이 Literal["HELLO"]이며, 이는 LiteralString과 호환된다는 것을 알고 있습니다. 다시 말해, capitalize 메서드는 LiteralString타입을 보존합니다. LiteralString을 보존하는 다른 str 메서드도 여러 개 있습니다.

메서드가 LiteralString을 보존하는 버전으로 오버로드되도록 typeshed의 str 스텁을 업데이트할 것을 제안합니다. 이는 타입 검사기가 각 메서드에 대한 LiteralString동작을 하드코딩할 필요가 없다는 것을 의미합니다. 또한 typeshed 스텁을 업데이트하여 향후 새로운 메서드도 쉽게 지원할 수 있습니다.

예를 들어, capitalize 메서드의 리터럴 타입을 보존하려면 아래와 같이 스텁을 변경합니다.

# before
def capitalize(self) -> str: ...

# after
@overload
def capitalize(self: LiteralString) -> LiteralString: ...
@overload
def capitalize(self) -> str: ...

str 스텁을 변경하면 스텁이 더 복잡해지고 오류 메시지를 이해하기 어려워질 수 있다는 단점이 있습니다. 타입 검사기는 사용자가 오류 메시지를 이해할 수 있도록 str을 특별히 처리해야 할 수도 있습니다.

아래에는 LiteralString타입의 인수와 함께 호출할 때 LiteralString을 반환하는 것으로 처리해야 하는 str 메서드의 전체 목록이 나와 있습니다. 이 PEP가 승인되면 typeshed에서 이러한 메서드 시그니처를 업데이트합니다.

@overload
def capitalize(self: LiteralString) -> LiteralString: ...
@overload
def capitalize(self) -> str: ...

@overload
def casefold(self: LiteralString) -> LiteralString: ...
@overload
def casefold(self) -> str: ...

@overload
def center(self: LiteralString, __width: SupportsIndex, __fillchar: LiteralString = ...) -> LiteralString: ...
@overload
def center(self, __width: SupportsIndex, __fillchar: str = ...) -> str: ...

if sys.version_info >= (3, 8):
    @overload
    def expandtabs(self: LiteralString, tabsize: SupportsIndex = ...) -> LiteralString: ...
    @overload
    def expandtabs(self, tabsize: SupportsIndex = ...) -> str: ...

else:
    @overload
    def expandtabs(self: LiteralString, tabsize: int = ...) -> LiteralString: ...
    @overload
    def expandtabs(self, tabsize: int = ...) -> str: ...

@overload
def format(self: LiteralString, *args: LiteralString, **kwargs: LiteralString) -> LiteralString: ...
@overload
def format(self, *args: str, **kwargs: str) -> str: ...

@overload
def join(self: LiteralString, __iterable: Iterable[LiteralString]) -> LiteralString: ...
@overload
def join(self, __iterable: Iterable[str]) -> str: ...

@overload
def ljust(self: LiteralString, __width: SupportsIndex, __fillchar: LiteralString = ...) -> LiteralString: ...
@overload
def ljust(self, __width: SupportsIndex, __fillchar: str = ...) -> str: ...

@overload
def lower(self: LiteralString) -> LiteralString: ...
@overload
def lower(self) -> LiteralString: ...

@overload
def lstrip(self: LiteralString, __chars: LiteralString | None = ...) -> LiteralString: ...
@overload
def lstrip(self, __chars: str | None = ...) -> str: ...

@overload
def partition(self: LiteralString, __sep: LiteralString) -> tuple[LiteralString, LiteralString, LiteralString]: ...
@overload
def partition(self, __sep: str) -> tuple[str, str, str]: ...

@overload
def replace(self: LiteralString, __old: LiteralString, __new: LiteralString, __count: SupportsIndex = ...) -> LiteralString: ...
@overload
def replace(self, __old: str, __new: str, __count: SupportsIndex = ...) -> str: ...

if sys.version_info >= (3, 9):
    @overload
    def removeprefix(self: LiteralString, __prefix: LiteralString) -> LiteralString: ...
    @overload
    def removeprefix(self, __prefix: str) -> str: ...

    @overload
    def removesuffix(self: LiteralString, __suffix: LiteralString) -> LiteralString: ...
    @overload
    def removesuffix(self, __suffix: str) -> str: ...

@overload
def rjust(self: LiteralString, __width: SupportsIndex, __fillchar: LiteralString = ...) -> LiteralString: ...
@overload
def rjust(self, __width: SupportsIndex, __fillchar: str = ...) -> str: ...

@overload
def rpartition(self: LiteralString, __sep: LiteralString) -> tuple[LiteralString, LiteralString, LiteralString]: ...
@overload
def rpartition(self, __sep: str) -> tuple[str, str, str]: ...

@overload
def rsplit(self: LiteralString, sep: LiteralString | None = ..., maxsplit: SupportsIndex = ...) -> list[LiteralString]: ...
@overload
def rsplit(self, sep: str | None = ..., maxsplit: SupportsIndex = ...) -> list[str]: ...

@overload
def rstrip(self: LiteralString, __chars: LiteralString | None = ...) -> LiteralString: ...
@overload
def rstrip(self, __chars: str | None = ...) -> str: ...

@overload
def split(self: LiteralString, sep: LiteralString | None = ..., maxsplit: SupportsIndex = ...) -> list[LiteralString]: ...
@overload
def split(self, sep: str | None = ..., maxsplit: SupportsIndex = ...) -> list[str]: ...

@overload
def splitlines(self: LiteralString, keepends: bool = ...) -> list[LiteralString]: ...
@overload
def splitlines(self, keepends: bool = ...) -> list[str]: ...

@overload
def strip(self: LiteralString, __chars: LiteralString | None = ...) -> LiteralString: ...
@overload
def strip(self, __chars: str | None = ...) -> str: ...

@overload
def swapcase(self: LiteralString) -> LiteralString: ...
@overload
def swapcase(self) -> str: ...

@overload
def title(self: LiteralString) -> LiteralString: ...
@overload
def title(self) -> str: ...

@overload
def upper(self: LiteralString) -> LiteralString: ...
@overload
def upper(self) -> str: ...

@overload
def zfill(self: LiteralString, __width: SupportsIndex) -> LiteralString: ...
@overload
def zfill(self, __width: SupportsIndex) -> str: ...

@overload
def __add__(self: LiteralString, __s: LiteralString) -> LiteralString: ...
@overload
def __add__(self, __s: str) -> str: ...

@overload
def __iter__(self: LiteralString) -> Iterator[str]: ...
@overload
def __iter__(self) -> Iterator[str]: ...

@overload
def __mod__(self: LiteralString, __x: Union[LiteralString, Tuple[LiteralString, ...]]) -> str: ...
@overload
def __mod__(self, __x: Union[str, Tuple[str, ...]]) -> str: ...

@overload
def __mul__(self: LiteralString, __n: SupportsIndex) -> LiteralString: ...
@overload
def __mul__(self, __n: SupportsIndex) -> str: ...

@overload
def __repr__(self: LiteralString) -> LiteralString: ...
@overload
def __repr__(self) -> str: ...

@overload
def __rmul__(self: LiteralString, n: SupportsIndex) -> LiteralString: ...
@overload
def __rmul__(self, n: SupportsIndex) -> str: ...

@overload
def __str__(self: LiteralString) -> LiteralString: ...
@overload
def __str__(self) -> str: ...

부록 D: 스텁에서 LiteralString을 사용하기 위한 지침

소스에 타입 어노테이션이 포함되지 않은 라이브러리는 Typeshed에서 타입 스텁을 지정할 수 있습니다. 머신 러닝용 라이브러리와 같이 다른 언어로 작성된 라이브러리도 Python 타입 스텁을 제공할 수 있습니다. 이는 타입 검사기가 타입 어노테이션이 소스 코드와 일치하는지 확인할 수 없으며 타입 스텁을 신뢰해야 한다는 것을 의미합니다. 따라서 타입 스텁 작성자는 LiteralString을 사용할 때 주의해야 합니다. 함수가 실제로는 안전하지 않은데도 안전한 것처럼 잘못 보일 수 있기 때문입니다.

스텁에서 LiteralString을 사용하기 위해 다음 지침을 권장합니다.

  • 스텁이 순수 함수에 대한 것이라면, 해당하는 모든 매개변수가 리터럴 타입(즉, LiteralString또는 Literal["a", "b"])을 가질 때에만 함수 또는 해당 오버로드의 반환 타입에 LiteralString을 사용할 것을 권장합니다.
    # OK
    @overload
    def my_transform(x: LiteralString, y: Literal["a", "b"]) -> LiteralString: ...
    @overload
    def my_transform(x: str, y: str) -> str: ...
    
    # Not OK
    @overload
    def my_transform(x: LiteralString, y: str) -> LiteralString: ...
    @overload
    def my_transform(x: str, y: str) -> str: ...
    
  • 스텁이 staticmethod에 대한 것이라면 위와 동일한 지침을 권장합니다.
  • 스텁이 그 밖의 어떤 종류의 메서드에 대한 것이라면 메서드 또는 해당 오버로드 중 어느 것의 반환 타입에도 LiteralString을 사용하지 않을 것을 권장합니다. 명시적인 매개변수가 모두 LiteralString타입이더라도 객체 자체는 사용자 데이터로 생성될 수 있으므로 반환 타입이 사용자에 의해 제어될 수 있기 때문입니다.
  • 스텁이 클래스 속성 또는 전역 변수에 대한 것이라면 타입이 지정되지 않은 코드가 속성에 임의의 값을 기록할 수 있으므로 LiteralString을 사용하지 않을 것도 권장합니다.

그러나 최종 결정은 라이브러리 작성자에게 맡깁니다. 메서드나 함수가 반환하는 문자열 또는 속성에 저장되는 문자열이 리터럴 타입을 갖는 것이 보장된다고 확신한다면 LiteralString을 사용할 수 있습니다. 즉, 문자열 리터럴에 리터럴을 보존하는 str 연산만 적용하여 문자열이 생성되는 경우입니다.

이러한 지침은 인라인 타입 어노테이션에는 적용되지 않는다는 점에 유의하십시오. 타입 검사기가 예를 들어 LiteralString을 반환하는 메서드가 실제로 해당 타입의 표현식을 반환하는지 확인할 수 있기 때문입니다.

참고 자료

Scala의 리터럴 문자열 타입

Scala는 싱글턴 타입의 상위 타입으로 uses Singleton을 사용하며, 여기에는 "foo"와 같은 리터럴 문자열 타입이 포함됩니다. Singleton은 이 PEP의 LiteralString에 해당하는 Scala의 일반화된 유사 개념입니다.

Tamer Abdulradi는 “컴파일 시점에 SQL 삽입 방지”를 위해 Scala의 리터럴 문자열 타입을 사용하는 방법을 Scala Days 강연인 Literal types: What are they good for? (슬라이드 52~68)에서 보여주었습니다.

감사합니다.

PEP에 대한 의견을 보내 주신 다음 분들께 감사드립니다:

Edward Qiu, Jia Chen, Shannon Zhu, Gregory P. Smith, Никита Соболев, CAM Gerlach, Arie Bovenberg, David Foster, and Shengye Wan