JustHTML validates and rewrites URL-valued attributes such as a[href] and img[src] according to URL policy rules.
For tag/attribute allowlists, inline styles, and unsafe-handling modes, see HTML Cleaning.
On this page:
JustHTML treats a set of attributes as URL-like (including href, src, srcset, action, and a few others).
URL-like attributes can trigger navigation, load resources, or execute scripts through unsafe schemes such as javascript:.
Different attributes need different rules. For example, you may allow navigation through a[href] while restricting img[src] to prevent remote requests and tracking. An explicit (tag, attr) rule defines which URLs you accept for each attribute.
For safety, these attributes are only kept if there is an explicit matching rule in UrlPolicy(allow_rules=...) for the (tag, attr) pair.
from justhtml import JustHTML, SanitizationPolicy
policy = SanitizationPolicy(
allowed_tags=["img"],
allowed_attributes={"img": ["src"]},
)
print(JustHTML("""
<img src="https://example.com">
<img src="https://attacker.com">
""", fragment=True, policy=policy).to_html())
<img>
<img>
This custom policy has no matching URL rules, so the sanitizer drops the URL-like attributes. To keep them, allow the attribute names and add matching rules in UrlPolicy:
from justhtml import JustHTML, SanitizationPolicy, UrlPolicy, UrlRule
policy = SanitizationPolicy(
allowed_tags=["img"],
allowed_attributes={"img": ["src"]},
url_policy=UrlPolicy(
allow_rules={
("img", "src"): UrlRule(
allowed_schemes={"https"},
allowed_hosts=["example.com"],
),
}
)
)
print(JustHTML("""
<img src="https://example.com">
<img src="http://example.com">
<img src="https://attacker.com">
""", fragment=True, policy=policy).to_html())
<img src="https://example.com">
<img>
<img>
For a URL-like attribute (like img[src] or a[href]) or a custom attribute with an explicit
UrlPolicy(allow_rules=...) entry, JustHTML applies these steps:
SanitizationPolicy.allowed_tags.SanitizationPolicy.allowed_attributes.UrlPolicy(allow_rules=...).UrlPolicy.url_filter runs and can rewrite or drop the value here).UrlRule.UrlRule.handling is set, it is appliedUrlPolicy.default_handling is appliedURL behavior is controlled by UrlPolicy:
default_handling: the default post-validation action for URL-like attributes that match an allow rule and do not set UrlRule.handling.default_allow_relative: whether relative URLs (like /path, ./path, ../path, ?q) are allowed by default.For URL-like attributes, or custom attributes that match an explicit (tag, attr) or ("*", attr) rule in
allow_rules, validated URLs use UrlPolicy.default_handling unless that specific UrlRule overrides it with
handling=....
Note: URL validation is always enforced by UrlRule.
from justhtml import UrlPolicy
UrlPolicy(
default_handling="allow", # or "strip" / "proxy"
default_allow_relative=True,
allow_rules={},
url_filter=None,
proxy=None,
)
"allow")With default_handling="allow", JustHTML keeps validated URLs whose attributes match an explicit (tag, attr) rule in UrlPolicy(allow_rules=...). Set UrlRule.handling to override that action for a rule.
"strip")Some renderers (notably email clients) want to avoid loading remote resources by default.
The built-in DEFAULT_POLICY already blocks remote image loads by default (img[src] only allows relative URLs).
To strip URL-valued attributes, choose one of these options:
(tag, attr) rule to drop that attribute.default_handling="strip" to drop attributes with matching rules unless a rule overrides the action.UrlRule(handling="strip") to drop a specific attribute.from justhtml import JustHTML, SanitizationPolicy, UrlPolicy, UrlRule
policy = SanitizationPolicy(
allowed_tags=["img"],
allowed_attributes={"*": [], "img": ["src"]},
url_policy=UrlPolicy(
default_handling="strip",
allow_rules={("img", "src"): UrlRule(allowed_schemes={"http", "https"})},
),
)
print(JustHTML('<img src="https://example.com/x">', fragment=True, policy=policy).to_html())
print(JustHTML('<img src="/x">', fragment=True, policy=policy).to_html())
<img>
<img>
If you instead want to block remote loads but allow relative image loads, configure the rule:
from justhtml import JustHTML, SanitizationPolicy, UrlPolicy, UrlRule
policy = SanitizationPolicy(
allowed_tags=["img"],
allowed_attributes={"*": [], "img": ["src"]},
url_policy=UrlPolicy(
default_handling="allow",
allow_rules={
("img", "src"): UrlRule(
allow_relative=True,
allowed_schemes=set(),
resolve_protocol_relative=None,
)
},
),
)
print(JustHTML('<img src="https://example.com/x">', fragment=True, policy=policy).to_html())
print(JustHTML('<img src="/x">', fragment=True, policy=policy).to_html())
<img>
<img src="/x">
"proxy")Instead of keeping URLs, you can rewrite them through a proxy endpoint:
from justhtml import JustHTML, SanitizationPolicy, UrlPolicy, UrlProxy, UrlRule
policy = SanitizationPolicy(
allowed_tags=["a"],
allowed_attributes={"*": [], "a": ["href"]},
url_policy=UrlPolicy(
default_handling="proxy",
proxy=UrlProxy(url="/proxy", param="url"),
allow_rules={
("a", "href"): UrlRule(allowed_schemes={"https"}),
},
),
)
print(JustHTML('<a href="https://example.com/?a=1&b=2">link</a>', policy=policy).to_html())
<a href="/proxy?url=https%3A%2F%2Fexample.com%2F%3Fa%3D1%26b%3D2">link</a>
Notes:
allow_relative=True.UrlPolicy.proxy) or per rule (UrlRule.proxy).Example: using a per-rule proxy override:
from justhtml import JustHTML, SanitizationPolicy, UrlPolicy, UrlProxy, UrlRule
policy = SanitizationPolicy(
allowed_tags=["a"],
allowed_attributes={"*": [], "a": ["href"]},
url_policy=UrlPolicy(
allow_rules={
("a", "href"): UrlRule(
handling="proxy",
allowed_schemes={"https"},
proxy=UrlProxy(url="/p", param="u"),
)
},
),
)
print(JustHTML('<a href="https://example.com/?a=1&b=2">link</a>', policy=policy).to_html())
<a href="/p?u=https%3A%2F%2Fexample.com%2F%3Fa%3D1%26b%3D2">link</a>
Protocol-relative URLs start with //. On HTTP and HTTPS pages, browsers resolve them using the page’s scheme.
By default, JustHTML resolves them to https before checking the allowed schemes. This prevents the URL from inheriting an insecure protocol from the embedding page.
You can configure this behavior per rule:
from justhtml import UrlRule
# Default behavior: resolve to https
rule = UrlRule(allowed_schemes=["https"], resolve_protocol_relative="https")
# Resolve to http
rule = UrlRule(allowed_schemes=["http", "https"], resolve_protocol_relative="http")
# Disallow protocol-relative URLs entirely
rule = UrlRule(allowed_schemes=["https"], resolve_protocol_relative=None)
JustHTML cannot preserve protocol-relative URLs unchanged. If you need this behavior, open an issue describing your use case.
srcset contains multiple URL candidates.
JustHTML parses the comma-separated candidates and sanitizes each candidate URL using the matching UrlRule for (tag, "srcset").
If any candidate is unsafe, the entire attribute is dropped.
UrlPolicy.url_filter can keep, rewrite, or drop a URL based on (tag, attr, value) before validation.
None to drop the attribute.Example: drop URLs to a blocked host:
from justhtml import JustHTML, SanitizationPolicy, UrlPolicy, UrlRule
def url_filter(tag: str, attr: str, value: str) -> str | None:
if "attacker.com" in value:
return None
return value
policy = SanitizationPolicy(
allowed_tags=["a"],
allowed_attributes={"*": [], "a": ["href"]},
url_policy=UrlPolicy(
url_filter=url_filter,
allow_rules={
("a", "href"): UrlRule(
allowed_schemes={"https"},
)
},
),
)
html = '<a href="https://example.com/">ok</a>\n<a href="https://attacker.com/">bad</a>'
print(JustHTML(html, fragment=True, policy=policy).to_html())
<a href="https://example.com/">ok</a>
<a>bad</a>
A UrlRule controls how a single URL-valued attribute is validated:
from justhtml import UrlRule
UrlRule(
allow_fragment=True,
resolve_protocol_relative="https",
allowed_schemes=set(),
allowed_hosts=None,
handling=None,
allow_relative=None,
proxy=None,
)
Field reference:
allow_fragment (default: True): allow same-document fragments like #section.resolve_protocol_relative (default: "https"): how to resolve protocol-relative URLs like //example.com before validation; set to None to reject them.allowed_schemes (default: set()): allowed schemes for absolute URLs (lowercased), e.g. {"https"}; empty means disallow all absolute URLs.allowed_hosts (default: None): optional host allowlist for absolute URLs; if set, the parsed host must be in this set.handling (default: None): optional handling override for an allowlisted attribute; "strip" drops it, "proxy" rewrites it, and None uses UrlPolicy.default_handling.allow_relative (default: None): optional override for UrlPolicy.default_allow_relative (relative URLs like /x, ./x, ?q).proxy (default: None): optional per-rule proxy config used when effective handling is "proxy" (overrides UrlPolicy.proxy).These are small UrlRule(...) building blocks that you can use in UrlPolicy(allow_rules={...}) for a specific (tag, attr) pair.
UrlRule(allowed_schemes={"https"})
UrlRule(allowed_schemes={"http", "https"})
UrlRule(allowed_schemes={"https"}, allowed_hosts={"example.com"})
UrlRule(
allowed_schemes={"https"},
allowed_hosts={"example.com", "static.example.com"},
)
UrlRule(allow_relative=True)
#section) and drop everything else:UrlRule(allow_fragment=True)
UrlRule(allowed_schemes={"https"}, allow_fragment=False)
UrlRule(
allow_relative=True,
allowed_schemes={"https"},
allowed_hosts={"example.com"},
)
//example.com) entirely:UrlRule(
allowed_schemes={"https"},
resolve_protocol_relative=None,
)
mailto: links:UrlRule(allowed_schemes={"mailto"})
tel: links:UrlRule(allowed_schemes={"tel"})
https: and mailto: (common for a[href]):UrlRule(allowed_schemes={"https", "mailto"}, resolve_protocol_relative="https")
UrlRule(handling="strip", allowed_schemes={"https"})
# Uses UrlPolicy.proxy (global proxy config), overriding any non-proxy default handling.
UrlRule(handling="proxy", allowed_schemes={"https"})
from justhtml import UrlProxy
# Uses a per-rule proxy override (UrlRule.proxy takes precedence over UrlPolicy.proxy)
UrlRule(
handling="proxy",
allowed_schemes={"https"},
proxy=UrlProxy(url="/proxy", param="url"),
)