ushka.guard.untrusted
The Untrusted string type and its propagation rules.
An Untrusted value is a str that remembers it came from outside the
program: an HTTP request, a cookie, a header, or the output of a language
model. It behaves exactly like str for reading, comparing, hashing and
serialising, so existing code keeps working. What changes is that the
dangerous sinks (raw HTML, redirects, file paths, raw SQL, ...) refuse it
until it goes through an explicit sanitiser from ushka.guard.safe.
Propagation
Operations that build a new string from an Untrusted value return an
Untrusted value too: + (on either side), *, % (with the
untrusted value on either side), slicing, .format()/.join() called on
it, and every case/whitespace/replace method. split and partition
return lists/tuples of Untrusted.
Known gaps (documented, tested as such)
CPython builds some strings in C without asking the operands, so the mark is lost in:
- f-strings with more than one part:
f"<b>{name}</b>" .formaton a trusted template with more than one part:"Hi {}".format(name)%with a tuple of arguments:"%s-%s" % (a, b).joincalled on a trusted separator:", ".join([name])str(value)(see the note on__str__below).encode()(bytes are not tracked)
ushka.guard.rewrite closes the first five for modules it compiles (Ushka
applies it to the files in routes/). Even so this is defence in depth,
not a proof. On Python 3.14+ prefer t-strings (t"<b>{name}</b>") for
HTML and SQL: they keep every interpolated value separate by construction.
This module has no dependency on the rest of Ushka on purpose.
class Untrusted(str)
A string whose content came from outside the program.
Create one with Untrusted(value, source): value is anything str()
accepts; source says where it came from (e.g. "query:next" or
"llm") and appears in error messages. If source is omitted and
value is already Untrusted, its source is kept.
describe(self) -> str
A debugging representation showing the value and its source.
split(self, sep: str | None = None, maxsplit: int = -1) -> list
rsplit(self, sep: str | None = None, maxsplit: int = -1) -> list
splitlines(self, keepends: bool = False) -> list
partition(self, sep: str) -> tuple
rpartition(self, sep: str) -> tuple
join(self, iterable: Iterable[str]) -> Untrusted
def taint(value: Any, source: str = UNKNOWN) -> Any
Marks value as untrusted if it is a string; returns other types as-is.
def taint_deep(value: Any, source: str = UNKNOWN) -> Any
Marks every string inside dicts/lists/tuples as untrusted.
Dictionary keys are marked too: they come from the same payload.
def is_untrusted(value: Any) -> bool
True if value is an Untrusted string.
def source_of(value: Any) -> str | None
The recorded source of an Untrusted value, or None.