miki-template 2.2.3 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.github/workflows/docs.yml +3 -1
  2. package/.github/workflows/release.yml +0 -5
  3. package/benchmarks/ejs-results.json +6 -6
  4. package/benchmarks/ejs.js +5 -3
  5. package/benchmarks/handlebars-results.json +6 -6
  6. package/benchmarks/handlebars.js +5 -8
  7. package/benchmarks/miki-results.json +6 -6
  8. package/benchmarks/miki.js +6 -3
  9. package/benchmarks/pug-results.json +6 -6
  10. package/benchmarks/pug.js +5 -3
  11. package/docs/api/async-render.md +88 -3
  12. package/docs/api/cache.md +90 -3
  13. package/docs/api/compile.md +131 -3
  14. package/docs/api/context-processors.md +80 -3
  15. package/docs/api/filters.md +223 -3
  16. package/docs/api/finder.md +97 -3
  17. package/docs/api/helpers.md +56 -3
  18. package/docs/api/i18n.md +160 -3
  19. package/docs/api/index.md +82 -28
  20. package/docs/api/libraries.md +210 -3
  21. package/docs/api/render-partial.md +84 -3
  22. package/docs/api/render.md +95 -3
  23. package/docs/api/security.md +148 -3
  24. package/docs/api/setup-express.md +78 -2
  25. package/docs/api/tags.md +138 -4
  26. package/docs/filter.md +0 -0
  27. package/docs/guide/advanced-usage.md +403 -6
  28. package/docs/guide/async-rendering.md +312 -4
  29. package/docs/guide/context-processors.md +261 -4
  30. package/docs/guide/custom-filters.md +315 -4
  31. package/docs/guide/custom-tags.md +275 -4
  32. package/docs/guide/filters.md +675 -3
  33. package/docs/guide/getting-started.md +109 -7
  34. package/docs/guide/installation.md +99 -4
  35. package/docs/guide/partial-templates.md +371 -4
  36. package/docs/guide/quick-start.md +228 -6
  37. package/docs/guide/security.md +348 -3
  38. package/docs/guide/tags.md +789 -6
  39. package/docs/guide/template-discovery.md +174 -4
  40. package/docs/guide/template-inheritance.md +277 -4
  41. package/docs/index.md +24 -42
  42. package/docs/integrations/elysia.md +4 -2
  43. package/docs/integrations/express.md +219 -219
  44. package/docs/integrations/fastify.md +4 -2
  45. package/docs/integrations/hono.md +4 -2
  46. package/docs/integrations/index.md +68 -68
  47. package/docs/integrations/koa.md +4 -2
  48. package/docs/integrations/nestjs.md +4 -2
  49. package/docs/integrations/tsed.md +4 -2
  50. package/docs/performance.md +45 -8
  51. package/ex.mjs +1 -1
  52. package/mkdocs.yml +0 -22
  53. package/overrides/main.html +1 -1
  54. package/package.json +1 -1
  55. package/requirements-docs.txt +2 -1
  56. package/src/codegen.js +905 -0
  57. package/src/context.js +42 -30
  58. package/src/filters.js +16 -0
  59. package/src/index.js +66 -61
  60. package/src/tags/control.js +15 -12
  61. package/src/utils.js +60 -0
  62. package/tests/filters.test.js +9 -0
  63. package/docs/javascripts/extra.js +0 -174
  64. package/docs/stylesheets/extra.css +0 -819
  65. package/overrides/partials/footer.html +0 -9
@@ -1,345 +1,690 @@
1
- # Security
1
+ # Security
2
+
3
+
2
4
 
3
5
  miki-template follows Django's security semantics to protect against common web vulnerabilities.
4
6
 
7
+
8
+
5
9
  ## Table of Contents
6
10
 
11
+
12
+
7
13
  - [Auto-Escaping](#auto-escaping)
14
+
8
15
  - [SafeString](#safestring)
16
+
9
17
  - [CSRF Protection](#csrf-protection)
18
+
10
19
  - [CSP Nonce](#csp-nonce)
20
+
11
21
  - [Path Traversal Protection](#path-traversal-protection)
22
+
12
23
  - [No Unsafe Code Execution](#no-unsafe-code-execution)
24
+
13
25
  - [HTML Escaping Details](#html-escaping-details)
26
+
14
27
  - [Context Processor Security](#context-processor-security)
15
28
 
29
+
30
+
16
31
  ---
17
32
 
33
+
34
+
18
35
  ## Auto-Escaping
19
36
 
37
+
38
+
20
39
  All variable output is HTML-escaped by default. This means any `<`, `>`, `&`, `"`, `'`, and `` ` `` characters in your data are converted to HTML entities before rendering.
21
40
 
41
+
42
+
22
43
  ```html
44
+
23
45
  {{ user_input }}
46
+
24
47
  ```
25
48
 
49
+
50
+
26
51
  If `user_input` is `<script>alert(1)</script>`, the output is:
27
52
 
53
+
54
+
28
55
  ```html
56
+
29
57
  &lt;script&gt;alert(&quot;1&quot;)&lt;/script&gt;
58
+
30
59
  ```
31
60
 
61
+
62
+
32
63
  This prevents XSS (Cross-Site Scripting) attacks where malicious users inject executable JavaScript.
33
64
 
65
+
66
+
34
67
  ### Disabling Auto-Escaping
35
68
 
69
+
70
+
36
71
  Use `{% autoescape off %}` to disable escaping for a block:
37
72
 
73
+
74
+
38
75
  ```html
76
+
39
77
  {% autoescape off %}
78
+
40
79
  {{ trusted_html }} {# not escaped #}
80
+
41
81
  {% endautoescape %}
82
+
42
83
  ```
43
84
 
85
+
86
+
44
87
  ### Re-enabling Auto-Escaping
45
88
 
89
+
90
+
46
91
  ```html
92
+
47
93
  {% autoescape on %}
94
+
48
95
  {{ user_input }} {# escaped again #}
96
+
49
97
  {% endautoescape %}
98
+
50
99
  ```
51
100
 
101
+
102
+
52
103
  **Real-world blog post:**
53
104
 
105
+
106
+
54
107
  ```html
108
+
55
109
  <article>
110
+
56
111
  <!-- Post body is trusted CMS content -->
112
+
57
113
  {% autoescape off %}
114
+
58
115
  {{ post.body_html }}
116
+
59
117
  {% endautoescape %}
60
118
 
119
+
120
+
61
121
  <!-- User comment is untrusted -->
122
+
62
123
  <div class="comments">
124
+
63
125
  {% for comment in comments %}
126
+
64
127
  <p>{{ comment.text }}</p>
128
+
65
129
  {% endfor %}
130
+
66
131
  </div>
132
+
67
133
  </article>
134
+
68
135
  ```
69
136
 
137
+
138
+
70
139
  ## SafeString
71
140
 
141
+
142
+
72
143
  Use the `safe` filter or `markSafe()` to mark content as trusted (bypassing auto-escaping):
73
144
 
145
+
146
+
74
147
  === "Template (safe filter)"
75
148
 
149
+
150
+
76
151
  ```html
152
+
77
153
  {{ trusted_html|safe }}
154
+
78
155
  ```
79
156
 
157
+
158
+
80
159
  === "CommonJS (markSafe)"
81
160
 
161
+
162
+
82
163
  ```javascript
164
+
83
165
  const { markSafe } = require('miki-template');
84
166
 
167
+
168
+
85
169
  const html = markSafe('<b>ok</b>');
170
+
86
171
  // Will not be escaped when rendered
172
+
87
173
  ```
88
174
 
175
+
176
+
89
177
  === "ES Modules (markSafe)"
90
178
 
179
+
180
+
91
181
  ```javascript
182
+
92
183
  import { markSafe } from 'miki-template';
93
184
 
185
+
186
+
94
187
  const html = markSafe('<b>ok</b>');
188
+
95
189
  ```
96
190
 
191
+
192
+
97
193
  ### SafeString Class
98
194
 
195
+
196
+
99
197
  You can also create `SafeString` instances directly:
100
198
 
199
+
200
+
101
201
  === "CommonJS"
102
202
 
203
+
204
+
103
205
  ```javascript
206
+
104
207
  const { SafeString } = require('miki-template');
105
208
 
209
+
210
+
106
211
  const html = new SafeString('<b>Bold</b>');
212
+
107
213
  // {{ html }} renders as <b>Bold</b>, NOT &lt;b&gt;Bold&lt;/b&gt;
214
+
108
215
  ```
109
216
 
217
+
218
+
110
219
  === "ES Modules"
111
220
 
221
+
222
+
112
223
  ```javascript
224
+
113
225
  import { SafeString } from 'miki-template';
114
226
 
227
+
228
+
115
229
  const html = new SafeString('<b>Bold</b>');
230
+
116
231
  ```
117
232
 
233
+
234
+
118
235
  ### Checking if a value is safe
119
236
 
237
+
238
+
120
239
  === "CommonJS"
121
240
 
241
+
242
+
122
243
  ```javascript
244
+
123
245
  const { isSafe } = require('miki-template');
124
246
 
247
+
248
+
125
249
  if (isSafe(value)) {
250
+
126
251
  // value is marked safe
252
+
127
253
  }
254
+
128
255
  ```
129
256
 
257
+
258
+
130
259
  === "ES Modules"
131
260
 
261
+
262
+
132
263
  ```javascript
264
+
133
265
  import { isSafe } from 'miki-template';
134
266
 
267
+
268
+
135
269
  if (isSafe(value)) {
270
+
136
271
  // value is marked safe
272
+
137
273
  }
274
+
138
275
  ```
139
276
 
277
+
278
+
140
279
  ## HTML Filters
141
280
 
281
+
282
+
142
283
  ### safe
143
284
 
285
+
286
+
144
287
  Mark a string as safe (no escaping):
145
288
 
289
+
290
+
146
291
  ```html
292
+
147
293
  {{ content|safe }}
294
+
148
295
  ```
149
296
 
297
+
298
+
150
299
  ### escape
151
300
 
301
+
302
+
152
303
  Force HTML escaping, even on SafeString values. This matches Django's `{{ value|escape }}` semantics:
153
304
 
305
+
306
+
154
307
  ```html
308
+
155
309
  <!-- Even if content is marked safe, escape forces HTML entities -->
310
+
156
311
  {{ content|escape }}
312
+
157
313
  ```
158
314
 
315
+
316
+
159
317
  **Real-world: render user-generated content with a safe wrapper**
160
318
 
319
+
320
+
161
321
  ```html
322
+
162
323
  <!-- In a filter -->
324
+
163
325
  {{ user.bio|default:"No bio yet."|escape }}
326
+
164
327
  ```
165
328
 
329
+
330
+
166
331
  ## CSRF Protection
167
332
 
333
+
334
+
168
335
  Use the `{% csrf_token %}` tag to output a hidden input with the CSRF token:
169
336
 
337
+
338
+
170
339
  ```html
340
+
171
341
  <form method="post">
342
+
172
343
  {% csrf_token %}
344
+
173
345
  <button type="submit">Submit</button>
346
+
174
347
  </form>
348
+
175
349
  ```
176
350
 
351
+
352
+
177
353
  The token value is HTML-escaped to prevent attribute injection. The output is:
178
354
 
355
+
356
+
179
357
  ```html
358
+
180
359
  <input type="hidden" name="csrfmiddlewaretoken" value="escaped_token_value">
360
+
181
361
  ```
182
362
 
363
+
364
+
183
365
  ### How it works
184
366
 
367
+
368
+
185
369
  - The tag looks for `csrf_token` in the template context.
370
+
186
371
  - If found, it outputs a hidden input with the escaped token value.
372
+
187
373
  - If not found, it outputs an empty hidden input.
188
374
 
375
+
376
+
189
377
  Provide `csrf_token` in context:
190
378
 
379
+
380
+
191
381
  === "CommonJS (Express + csurf)"
192
382
 
383
+
384
+
193
385
  ```javascript
386
+
194
387
  const csrf = require('csurf');
195
388
 
389
+
390
+
196
391
  app.use(csrf({ cookie: true }));
392
+
197
393
  app.use((req, res, next) => {
394
+
198
395
  res.locals.csrf_token = req.csrfToken();
396
+
199
397
  next();
398
+
200
399
  });
400
+
201
401
  ```
202
402
 
403
+
404
+
203
405
  === "ES Modules"
204
406
 
407
+
408
+
205
409
  ```javascript
410
+
206
411
  app.use((req, res, next) => {
412
+
207
413
  res.locals.csrf_token = req.csrfToken();
414
+
208
415
  next();
416
+
209
417
  });
418
+
210
419
  ```
211
420
 
421
+
422
+
212
423
  ## CSP Nonce
213
424
 
425
+
426
+
214
427
  Use the `{% csp_nonce_attr %}` tag to output a `nonce` attribute when `csp_nonce` is in the context. This is essential for Content-Security-Policy-compliant inline scripts:
215
428
 
429
+
430
+
216
431
  ```html
432
+
217
433
  <script {% csp_nonce_attr %} src="/js/app.js"></script>
434
+
218
435
  ```
219
436
 
437
+
438
+
220
439
  If `csp_nonce` is present in context, the output is:
221
440
 
441
+
442
+
222
443
  ```html
444
+
223
445
  <script nonce="abc123" src="/js/app.js"></script>
446
+
224
447
  ```
225
448
 
449
+
450
+
226
451
  If `csp_nonce` is missing, the tag outputs nothing — the `<script>` tag is rendered without a nonce.
227
452
 
453
+
454
+
228
455
  Provide `csp_nonce` in context:
229
456
 
457
+
458
+
230
459
  === "CommonJS"
231
460
 
461
+
462
+
232
463
  ```javascript
464
+
233
465
  app.use((req, res, next) => {
466
+
234
467
  res.locals.csp_nonce = crypto.randomBytes(16).toString('base64');
468
+
235
469
  next();
470
+
236
471
  });
472
+
237
473
  ```
238
474
 
475
+
476
+
239
477
  === "ES Modules"
240
478
 
479
+
480
+
241
481
  ```javascript
482
+
242
483
  import crypto from 'node:crypto';
243
484
 
485
+
486
+
244
487
  app.use((req, res, next) => {
488
+
245
489
  res.locals.csp_nonce = crypto.randomBytes(16).toString('base64');
490
+
246
491
  next();
492
+
247
493
  });
494
+
248
495
  ```
249
496
 
497
+
498
+
250
499
  ## Path Traversal Protection
251
500
 
501
+
502
+
252
503
  `{% extends %}`, `{% include %}`, and `{% extends %}` paths are validated to prevent directory traversal attacks:
253
504
 
505
+
506
+
254
507
  ```html
508
+
255
509
  {% extends "../../etc/passwd" %} {# REJECTED #}
510
+
256
511
  {% include "../../secrets" %} {# REJECTED #}
512
+
257
513
  ```
258
514
 
515
+
516
+
259
517
  The engine checks that resolved paths stay within the allowed views directories. An error with message starting with `path traversal` is thrown if the resolved path escapes the views root.
260
518
 
519
+
520
+
261
521
  ## No Unsafe Code Execution
262
522
 
523
+
524
+
263
525
  miki-template never uses `eval()`. Expressions are parsed and evaluated safely using the AST-based expression evaluator. This prevents code injection attacks — template expressions like `{{ user.name }}` are resolved through property lookups, never by executing arbitrary JavaScript.
264
526
 
527
+
528
+
265
529
  ## HTML Escaping Details
266
530
 
531
+
532
+
267
533
  miki-template uses the [`he`](https://github.com/mathiasbynetworks/he) library for HTML escaping, which converts:
268
534
 
535
+
536
+
269
537
  | Character | Escaped |
538
+
270
539
  |-----------|---------|
540
+
271
541
  | `&` | `&amp;` |
542
+
272
543
  | `<` | `&lt;` |
544
+
273
545
  | `>` | `&gt;` |
546
+
274
547
  | `"` | `&quot;` |
548
+
275
549
  | `'` | `&#x27;` |
550
+
276
551
  | `` ` `` | `&#96;` |
277
552
 
553
+
554
+
278
555
  ```javascript
556
+
279
557
  // Access escaping directly
558
+
280
559
  const { escapeHtml } = require('miki-template');
560
+
281
561
  // or
562
+
282
563
  import { escapeHtml } from 'miki-template';
283
564
 
565
+
566
+
284
567
  const escaped = escapeHtml('<script>alert("xss")</script>');
568
+
285
569
  // → "&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;"
286
570
 
571
+
572
+
287
573
  // Force-escape even SafeString values (third argument)
574
+
288
575
  const reescaped = escapeHtml(safeStringInstance, true);
576
+
289
577
  ```
290
578
 
579
+
580
+
291
581
  ### Programmatic Escaping
292
582
 
583
+
584
+
293
585
  === "CommonJS"
294
586
 
587
+
588
+
295
589
  ```javascript
590
+
296
591
  const { escapeHtml } = require('miki-template');
297
592
 
593
+
594
+
298
595
  const escaped = escapeHtml('<script>');
596
+
299
597
  // Output: &lt;script&gt;
598
+
300
599
  ```
301
600
 
601
+
602
+
302
603
  === "ES Modules"
303
604
 
605
+
606
+
304
607
  ```javascript
608
+
305
609
  import { escapeHtml } from 'miki-template';
306
610
 
611
+
612
+
307
613
  const escaped = escapeHtml('<script>');
614
+
308
615
  // Output: &lt;script&gt;
616
+
309
617
  ```
310
618
 
619
+
620
+
311
621
  ## Context Processor Security
312
622
 
623
+
624
+
313
625
  Context processors run before every render and can inject global variables. Be careful not to expose sensitive data:
314
626
 
627
+
628
+
315
629
  === "CommonJS"
316
630
 
631
+
632
+
317
633
  ```javascript
634
+
318
635
  const { registerContextProcessor } = require('miki-template');
319
636
 
637
+
638
+
320
639
  registerContextProcessor((context) => {
640
+
321
641
  return {
642
+
322
643
  siteName: 'My App',
644
+
323
645
  // Don't inject secrets here - they'll be available in ALL templates
646
+
324
647
  };
648
+
325
649
  });
650
+
326
651
  ```
327
652
 
653
+
654
+
328
655
  === "ES Modules"
329
656
 
657
+
658
+
330
659
  ```javascript
660
+
331
661
  import { registerContextProcessor } from 'miki-template';
332
662
 
663
+
664
+
333
665
  registerContextProcessor((context) => {
666
+
334
667
  return {
668
+
335
669
  siteName: 'My App',
670
+
336
671
  };
672
+
337
673
  });
674
+
338
675
  ```
339
676
 
677
+
678
+
340
679
  **Key behavior:** Context processor values respect Django semantics — existing context values **win** over processor defaults. If you render with `{ user: req.user }` and a processor returns `{ user: 'Guest' }`, the explicit `req.user` is preserved.
341
680
 
681
+
682
+
342
683
  ## Next Steps
343
684
 
344
- - [Integrations](../integrations/)
345
- - [API Reference: Security](../api/security)
685
+
686
+
687
+ - [Integrations](../integrations/index.md)
688
+
689
+ - [API Reference: Security](../api/security.md)
690
+