Skip to content

Conversation

@jg-rp
Copy link
Owner

@jg-rp jg-rp commented Jul 24, 2025

Inspired by https://github.com/salesforce/secure-filters and Django's escapejs filter, this PR adds an escapejs filter for escaping characters for use in JavaScript string literals.

Whereas the standard escape filter replaces &, <, >, ' and " with their equivalent HTML escape sequences, escapejs replaces control characters and potentially dangerous symbols with their corresponding Unicode escape sequences.

from liquid import render

template = """\
{% assign some_string = "<script>alert('x')</script>" -%}
<img src="" onerror="{{ some_string | escapejs }}" />
"""

print(render(template))
# <img src="" onerror="\u003Cscript\u003Ealert(\u0027x\u0027)\u003C/script\u003E" />

Note that escapejs is enabled by default. Like the safe filter, it does not need to be added to your Liquid environment. You can delete escapejs and/or safe from your Liquid environment if desired.

from liquid import Environment

env = Environment()
del env.tags["escapejs"]
del env.tags["safe"]

# env.render(...)

@jg-rp
Copy link
Owner Author

jg-rp commented Jul 24, 2025

Some performance notes for our escapejs function.

On an old Linux machine with the test string "This is a test with <script>alert('xss');</script> \u2028 \u2029" escaped 100,000 times (best of 5 runs).

Python Version Django-style translate Manual str.join loop Regex-based re.sub
3.9 0.290919s 1.235294s 0.348518s
3.10 0.308623s 1.149523s 0.350728s
3.11 0.284580s 0.845416s 0.296460s
3.12 0.531266s 0.911097s 0.355675s
3.13 0.477509s 0.738343s 0.315116s
3.14 0.158303s 0.867450s 0.350290s
PyPy 3.10 0.985162s 0.345929s 0.100031s

On a Mac Mini M2 with the test string "This is a test with <script>alert('xss');</script> \u2028 \u2029" escaped 100,000 times (best of 5 runs).

Python Version Django-style translate Manual str.join loop Regex-based re.sub
3.11 0.138064s 0.505856s 0.144051s
3.12 0.295304s 0.561523s 0.167004s

As strings get longer, the regex approach seems to be the clear winner.

On the same Linux machine with the test string "<strong>Hello, World!</strong>" + "x" * 100_000 escaped 100 times (best of 5 runs).

Python Version Django-style translate Manual str.join loop Regex-based re.sub
3.9 0.400459s 1.368443s 0.045862s
3.10 0.452317s 1.505935s 0.051953s
3.11 0.386549s 1.021357s 0.032609s
3.12 0.964879s 1.009775s 0.042083s
3.13 0.861251s 0.814461s 0.052213s
3.14 0.152441s 0.923251s 0.051938s
PyPy 3.10 2.073689s 0.297744s 0.011421s

On a Mac Mini M2 with the test string "<strong>Hello, World!</strong>" + "x" * 100_000 escaped 100 times (best of 5 runs).

Python Version Django-style translate Manual str.join loop Regex-based re.sub
3.11 0.201654s 0.604399s 0.038586s
3.12 0.539468s 0.674371s 0.028565s

@jg-rp jg-rp marked this pull request as ready for review July 25, 2025 06:35
@jg-rp
Copy link
Owner Author

jg-rp commented Jul 25, 2025

@stash-benchling, any feedback is welcome.

@jg-rp jg-rp merged commit 8effde2 into main Jul 30, 2025
36 checks passed
@jg-rp jg-rp deleted the escapejs branch July 30, 2025 16:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants