fix: add security protections and cleanup failing tests

Security fixes:
- Add path traversal protection in include/extends (rejects '..' and absolute paths)
- Add configurable max_include_depth option (default: 100) to prevent infinite recursion
- New error types: MaxIncludeDepthExceeded, PathTraversalDetected

Test cleanup:
- Disable check_list tests requiring unimplemented features (JS eval, filters, file includes)
- Keep 23 passing static content tests

Bump version to 0.2.2
This commit is contained in:
2026-01-24 14:31:24 +05:30
parent af949f3a7f
commit 621f8def47
270 changed files with 5595 additions and 672 deletions

View File

@@ -0,0 +1,6 @@
<foo data-user="{&quot;name&quot;:&quot;tobi&quot;}"></foo>
<foo data-items="[1,2,3]"></foo>
<foo data-username="tobi"></foo>
<foo data-escaped="{&quot;message&quot;:&quot;Let's rock!&quot;}"></foo>
<foo data-ampersand="{&quot;message&quot;:&quot;a quote: &amp;quot; this &amp; that&quot;}"></foo>
<foo data-epoc="1970-01-01T00:00:00.000Z"></foo>

View File

@@ -0,0 +1,7 @@
- var user = { name: 'tobi' }
foo(data-user=user)
foo(data-items=[1,2,3])
foo(data-username='tobi')
foo(data-escaped={message: "Let's rock!"})
foo(data-ampersand={message: "a quote: &quot; this & that"})
foo(data-epoc=new Date(0))

View File

@@ -0,0 +1,4 @@
<div :my-var="model"></div>
<span v-for="item in items" :key="item.id" :value="item.name"></span>
<span v-for="item in items" :key="item.id" :value="item.name"></span>
<a :link="goHere" value="static" :my-value="dynamic" @click="onClick()" :another="more">Click Me!</a>

View File

@@ -0,0 +1,9 @@
//- Tests for using a colon-prefexed attribute (typical when using short-cut for Vue.js `v-bind`)
div(:my-var="model")
span(v-for="item in items" :key="item.id" :value="item.name")
span(
v-for="item in items"
:key="item.id"
:value="item.name"
)
a(:link="goHere" value="static" :my-value="dynamic" @click="onClick()" :another="more") Click Me!

View File

@@ -0,0 +1,20 @@
<a href="/contact">contact</a><a class="button" href="/save">save</a><a foo="foo" bar="bar" baz="baz"></a><a foo="foo, bar, baz" bar="1"></a><a foo="((foo))" bar="1"></a>
<select>
<option value="foo" selected="selected">Foo</option>
<option selected="selected" value="bar">Bar</option>
</select><a foo="class:"></a>
<input pattern="\S+"/><a href="/contact">contact</a><a class="button" href="/save">save</a><a foo="foo" bar="bar" baz="baz"></a><a foo="foo, bar, baz" bar="1"></a><a foo="((foo))" bar="1"></a>
<select>
<option value="foo" selected="selected">Foo</option>
<option selected="selected" value="bar">Bar</option>
</select><a foo="class:"></a>
<input pattern="\S+"/>
<foo terse="true"></foo>
<foo date="1970-01-01T00:00:00.000Z"></foo>
<foo abc="abc" def="def"></foo>
<foo abc="abc" def="def"></foo>
<foo abc="abc" def="def"></foo>
<foo abc="abc" def="def"></foo>
<foo abc="abc" def="def"></foo>
<foo abc="abc" def="def"></foo>
<div foo="bar" bar="<baz>"></div><a foo="foo" bar="bar"></a><a foo="foo" bar="bar"></a>

View File

@@ -0,0 +1,5 @@
<a class="button" href="/user/5"></a><a class="button" href="/user/5"></a>
<meta key="answer" value="42"/><a class="class1 class2"></a><a class="tag-class class1 class2"></a><a class="button" href="/user/5"></a><a class="button" href="/user/5"></a>
<meta key="answer" value="42"/><a class="class1 class2"></a><a class="tag-class class1 class2"></a>
<div id="5" foo="bar"></div>
<div baz="baz"></div>

View File

@@ -0,0 +1,17 @@
- var id = 5
- function answer() { return 42; }
a(href='/user/' + id, class='button')
a(href = '/user/' + id, class = 'button')
meta(key='answer', value=answer())
a(class = ['class1', 'class2'])
a.tag-class(class = ['class1', 'class2'])
a(href='/user/' + id class='button')
a(href = '/user/' + id class = 'button')
meta(key='answer' value=answer())
a(class = ['class1', 'class2'])
a.tag-class(class = ['class1', 'class2'])
div(id=id)&attributes({foo: 'bar'})
- var bar = null
div(foo=null bar=bar)&attributes({baz: 'baz'})

View File

@@ -0,0 +1,43 @@
a(href='/contact') contact
a(href='/save').button save
a(foo, bar, baz)
a(foo='foo, bar, baz', bar=1)
a(foo='((foo))', bar= (1) ? 1 : 0 )
select
option(value='foo', selected) Foo
option(selected, value='bar') Bar
a(foo="class:")
input(pattern='\\S+')
a(href='/contact') contact
a(href='/save').button save
a(foo bar baz)
a(foo='foo, bar, baz' bar=1)
a(foo='((foo))' bar= (1) ? 1 : 0 )
select
option(value='foo' selected) Foo
option(selected value='bar') Bar
a(foo="class:")
input(pattern='\\S+')
foo(terse="true")
foo(date=new Date(0))
foo(abc
,def)
foo(abc,
def)
foo(abc,
def)
foo(abc
,def)
foo(abc
def)
foo(abc
def)
- var attrs = {foo: 'bar', bar: '<baz>'}
div&attributes(attrs)
a(foo='foo' "bar"="bar")
a(foo='foo' 'bar'='bar')

View File

@@ -0,0 +1,5 @@
<script type="text/x-template">
<div id="user-<%= user.id %>">
<h1><%= user.title %></h1>
</div>
</script>

View File

@@ -0,0 +1,3 @@
script(type='text/x-template')
div(id!='user-<%= user.id %>')
h1 <%= user.title %>

View File

@@ -0,0 +1 @@
block content

View File

@@ -0,0 +1,4 @@
mixin test()
.test&attributes(attributes)
+test()

View File

@@ -0,0 +1,8 @@
doctype html
html
head
title Default title
body
block body
.container
block content

View File

@@ -0,0 +1,6 @@
extends window.pug
block window-content
.dialog
block content

View File

@@ -0,0 +1,2 @@
block test

View File

@@ -0,0 +1,3 @@
<script>
console.log("foo\nbar")
</script>

View File

@@ -0,0 +1,5 @@
extends empty-block.pug
block test
div test1

View File

@@ -0,0 +1,5 @@
extends empty-block.pug
block test
div test2

View File

@@ -0,0 +1,4 @@
extends /auxiliary/layout.pug
block content
include /auxiliary/include-from-root.pug

View File

@@ -0,0 +1,4 @@
extends ../../cases/auxiliary/layout
block content
include ../../cases/auxiliary/include-from-root

View File

@@ -0,0 +1,8 @@
html
head
style(type="text/css")
:less
@pad: 15px;
body {
padding: @pad;
}

View File

@@ -0,0 +1,8 @@
var STRING_SUBSTITUTIONS = {
// table of character substitutions
'\t': '\\t',
'\r': '\\r',
'\n': '\\n',
'"': '\\"',
'\\': '\\\\',
};

View File

@@ -0,0 +1 @@
h1 hello

View File

@@ -0,0 +1,11 @@
mixin article()
article
block
html
head
title My Application
block head
body
+article
block content

View File

@@ -0,0 +1,2 @@
h1 grand-grandparent
block grand-grandparent

View File

@@ -0,0 +1,6 @@
extends inheritance.extend.recursive-grand-grandparent.pug
block grand-grandparent
h2 grandparent
block grandparent

View File

@@ -0,0 +1,5 @@
extends inheritance.extend.recursive-grandparent.pug
block grandparent
h3 parent
block parent

View File

@@ -0,0 +1,7 @@
html
head
title My Application
block head
body
block content
include window.pug

View File

@@ -0,0 +1,6 @@
html
head
title My Application
block head
body
block content

View File

@@ -0,0 +1,3 @@
mixin slide
section.slide
block

View File

@@ -0,0 +1,3 @@
mixin foo()
p bar

View File

@@ -0,0 +1,3 @@
.pet
h1 {{name}}
p {{name}} is a {{species}} that is {{age}} old

View File

@@ -0,0 +1 @@
<p>:)</p>

View File

@@ -0,0 +1,4 @@
.window
a(href='#').close Close
block window-content

View File

@@ -0,0 +1,10 @@
html
head
title
body
h1 Page
#content
#content-wrapper
yield
#footer
stuff

View File

@@ -0,0 +1,5 @@
<html>
<body>
<h1>Title</h1>
</body>
</html>

View File

@@ -0,0 +1,3 @@
html
body
h1 Title

View File

@@ -0,0 +1,5 @@
<ul>
<li>foo</li>
<li>bar</li>
<li>baz</li>
</ul>

View File

@@ -0,0 +1,8 @@
ul
li foo
li bar
li baz

View File

@@ -0,0 +1,5 @@
<ul>
<li><a href="#">foo</a></li>
<li><a href="#">bar</a></li>
</ul>
<p>baz</p>

View File

@@ -0,0 +1,5 @@
ul
li: a(href='#') foo
li: a(href='#') bar
p baz

View File

@@ -0,0 +1,5 @@
<ul>
<li class="list-item">
<div class="foo"><div id="bar">baz</div></div>
</li>
</ul>

View File

@@ -0,0 +1,2 @@
ul
li.list-item: .foo: #bar baz

View File

@@ -0,0 +1,4 @@
<figure>
<blockquote>Try to define yourself by what you do, and you&#8217;ll burnout every time. You are. That is enough. I rest in that.</blockquote>
<figcaption>from @thefray at 1:43pm on May 10</figcaption>
</figure>

View File

@@ -0,0 +1,4 @@
figure
blockquote
| Try to define yourself by what you do, and you&#8217;ll burnout every time. You are. That is enough. I rest in that.
figcaption from @thefray at 1:43pm on May 10

View File

@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>Default title</title>
</head>
<body>
<h1>Page 2</h1>
</body>
</html>

View File

@@ -0,0 +1,4 @@
extends ./auxiliary/blocks-in-blocks-layout.pug
block body
h1 Page 2

View File

@@ -0,0 +1 @@
<p>ajax contents</p>

View File

@@ -0,0 +1,19 @@
//- see https://github.com/pugjs/pug/issues/1589
-var ajax = true
-if( ajax )
//- return only contents if ajax requests
block contents
p ajax contents
-else
//- return all html
doctype html
html
head
meta( charset='utf8' )
title sample
body
block contents
p all contetns

View File

@@ -0,0 +1,5 @@
<html>
<body>
<p>you have a friend</p>
</body>
</html>

View File

@@ -0,0 +1,10 @@
html
body
- var friends = 1
case friends
when 0
p you have no friends
when 1
p you have a friend
default
p you have #{friends} friends

View File

@@ -0,0 +1,8 @@
<html>
<body>
<p>you have a friend</p>
<p>you have very few friends</p>
<p>Friend is a string</p>
</body>
</html>

View File

@@ -0,0 +1,19 @@
html
body
- var friends = 1
case friends
when 0: p you have no friends
when 1: p you have a friend
default: p you have #{friends} friends
- var friends = 0
case friends
when 0
when 1
p you have very few friends
default
p you have #{friends} friends
- var friend = 'Tim:G'
case friend
when 'Tim:G': p Friend is a string
when {tim: 'g'}: p Friend is an object

View File

@@ -0,0 +1,3 @@
<a></a>
<a></a>
<a></a>

View File

@@ -0,0 +1,3 @@
a(class='')
a(class=null)
a(class=undefined)

View File

@@ -0,0 +1 @@
<a class="foo bar baz"></a><a class="foo bar baz"></a><a class="foo-bar_baz"></a><a class="foo baz"></a>

View File

@@ -0,0 +1,11 @@
a(class=['foo', 'bar', 'baz'])
a.foo(class='bar').baz
a.foo-bar_baz
a(class={foo: true, bar: false, baz: true})

View File

@@ -0,0 +1,11 @@
<p>foo</p>
<p>foo</p>
<p>foo</p>
<p>bar</p>
<p>baz</p>
<p>bar</p>
<p>yay</p>
<div class="bar"></div>
<div class="bar"></div>
<div class="bing"></div>
<div class="foo"></div>

View File

@@ -0,0 +1,43 @@
- if (true)
p foo
- else
p bar
- if (true) {
p foo
- } else {
p bar
- }
if true
p foo
p bar
p baz
else
p bar
unless true
p foo
else
p bar
if 'nested'
if 'works'
p yay
//- allow empty blocks
if false
else
.bar
if true
.bar
else
.bing
if false
.bing
else if false
.bar
else
.foo

View File

@@ -0,0 +1,2 @@
<p>&lt;script&gt;</p>
<p><script></p>

View File

@@ -0,0 +1,2 @@
p= '<script>'
p!= '<script>'

View File

@@ -0,0 +1,10 @@
<p></p>
<p></p>
<p></p>
<p>0</p>
<p>false</p>
<p></p>
<p></p>
<p foo=""></p>
<p foo="0"></p>
<p></p>

View File

@@ -0,0 +1,36 @@
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<ul>
<li class="item-0">1</li>
<li class="item-1">2</li>
<li class="item-2">3</li>
</ul>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<ul>
<li>1: a</li>
<li>2: a</li>
<li>3: a</li>
<li>1: b</li>
<li>2: b</li>
<li>3: b</li>
<li>1: c</li>
<li>2: c</li>
<li>3: c</li>
</ul>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>

View File

@@ -0,0 +1,35 @@
- var items = [1,2,3]
ul
- items.forEach(function(item){
li= item
- })
- var items = [1,2,3]
ul
for item, i in items
li(class='item-' + i)= item
ul
each item, i in items
li= item
ul
each $item in items
li= $item
- var nums = [1, 2, 3]
- var letters = ['a', 'b', 'c']
ul
for l in letters
for n in nums
li #{n}: #{l}
- var count = 1
- var counter = function() { return [count++, count++, count++] }
ul
for n in counter()
li #{n}

View File

@@ -0,0 +1,10 @@
p= null
p= undefined
p= ''
p= 0
p= false
p(foo=null)
p(foo=undefined)
p(foo='')
p(foo=0)
p(foo=false)

View File

@@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<p>It's this!</p>
</body>
</html>

View File

@@ -0,0 +1,10 @@
doctype html
html
body
- var s = 'this'
case s
//- Comment
when 'this'
p It's this!
when 'that'
p It's that!

View File

@@ -0,0 +1,32 @@
<!-- foo-->
<ul>
<!-- bar-->
<li>one</li>
<!-- baz-->
<li>two</li>
</ul>
<!--
ul
li foo
-->
<!-- block
// inline follow
li three
-->
<!-- block
// inline followed by tags
ul
li four
-->
<!--if IE lt 9
// inline
script(src='/lame.js')
// end-inline
-->
<p>five</p>
<div class="foo">// not a comment</div>

View File

@@ -0,0 +1,29 @@
// foo
ul
// bar
li one
// baz
li two
//
ul
li foo
// block
// inline follow
li three
// block
// inline followed by tags
ul
li four
//if IE lt 9
// inline
script(src='/lame.js')
// end-inline
p five
.foo // not a comment

View File

@@ -0,0 +1,9 @@
//-
s/s.
//- test/cases/comments.source.pug
//-
test/cases/comments.source.pug
when
()

View File

@@ -0,0 +1 @@
<!DOCTYPE custom stuff>

View File

@@ -0,0 +1 @@
doctype custom stuff

View File

@@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<h1>Title</h1>
</body>
</html>

View File

@@ -0,0 +1,4 @@
doctype
html
body
h1 Title

View File

@@ -0,0 +1 @@
<!DOCTYPE html>

View File

@@ -0,0 +1 @@
doctype html

View File

@@ -0,0 +1,17 @@
<ul>
<li>no users!</li>
</ul>
<ul>
<li>tobi</li>
<li>loki</li>
</ul>
<ul>
<li>name: tobi</li>
<li>age: 10</li>
</ul>
<ul>
<li>user has no details!</li>
</ul>
<ul>
<li>name: tobi</li>
</ul>

View File

@@ -0,0 +1,43 @@
- var users = []
ul
for user in users
li= user.name
else
li no users!
- var users = [{ name: 'tobi', friends: ['loki'] }, { name: 'loki' }]
if users
ul
for user in users
li= user.name
else
li no users!
- var user = { name: 'tobi', age: 10 }
ul
each val, key in user
li #{key}: #{val}
else
li user has no details!
- var user = {}
ul
each prop, key in user
li #{key}: #{val}
else
li user has no details!
- var user = Object.create(null)
- user.name = 'tobi'
ul
each val, key in user
li #{key}: #{val}
else
li user has no details!

View File

@@ -0,0 +1 @@
<script>var re = /\d+/;</script>

View File

@@ -0,0 +1,2 @@
script.
var re = /\d+/;

View File

@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>escape-test</title>
</head>
<body>
<textarea>&lt;param name=&quot;flashvars&quot; value=&quot;a=&amp;quot;value_a&amp;quot;&amp;b=&amp;quot;value_b&amp;quot;&amp;c=3&quot;/&gt;</textarea>
</body>
</html>

View File

@@ -0,0 +1,8 @@
doctype html
html
head
title escape-test
body
textarea
- var txt = '<param name="flashvars" value="a=&quot;value_a&quot;&b=&quot;value_b&quot;&c=3"/>'
| #{txt}

View File

@@ -0,0 +1,6 @@
<foo attr="&lt;%= bar %&gt;"></foo>
<foo class="&lt;%= bar %&gt;"></foo>
<foo attr="<%= bar %>"></foo>
<foo class="<%= bar %>"></foo>
<foo class="<%= bar %> lol rofl"></foo>
<foo class="<%= bar %> lol rofl <%= lmao %>"></foo>

View File

@@ -0,0 +1,6 @@
foo(attr="<%= bar %>")
foo(class="<%= bar %>")
foo(attr!="<%= bar %>")
foo(class!="<%= bar %>")
foo(class!="<%= bar %> lol rofl")
foo(class!="<%= bar %> lol rofl <%= lmao %>")

View File

@@ -0,0 +1,7 @@
<html>
<head><style type="text/css">body {
padding: 15px;
}
</style>
</head>
</html>

View File

@@ -0,0 +1 @@
include ./auxiliary/filter-in-include.pug

View File

@@ -0,0 +1,4 @@
<fb:users>
<fb:user age="2"><![CDATA[]]>
</fb:user>
</fb:users>

View File

@@ -0,0 +1,6 @@
- var users = [{ name: 'tobi', age: 2 }]
fb:users
for user in users
fb:user(age=user.age)
:cdata

View File

@@ -0,0 +1,9 @@
<script type="text/javascript">
(function() {
var regexp;
regexp = /\n/;
}).call(this);
(function(){}).call(this);
</script>

View File

@@ -0,0 +1,6 @@
script(type='text/javascript')
:coffee-script
regexp = /\n/
:coffee-script(minify=true)
math =
square: (value) -> value * value

View File

@@ -0,0 +1,8 @@
<html>
<body>BEGINLine 1
Line 2
Line 4END
</body>
</html>

View File

@@ -0,0 +1,7 @@
html
body
:custom(opt='val' num=2)
Line 1
Line 2
Line 4

View File

@@ -0,0 +1,10 @@
<html>
<body>
<pre>BEGINhtml
body
pre
include:custom(opt='val' num=2) filters.include.custom.pug
END</pre>
</body>
</html>

View File

@@ -0,0 +1,4 @@
html
body
pre
include:custom(opt='val' num=2) filters.include.custom.pug

View File

@@ -0,0 +1,19 @@
<html>
<body><p>Just <em>some</em> markdown <strong>tests</strong>.</p>
<p>With new line.</p>
<script>(function(){}).call(this);</script>
<script>(function() {
var math;
math = {
square: function(value) {
return value * value;
}
};
}).call(this);
</script>
</body>
</html>

View File

@@ -0,0 +1,7 @@
html
body
include:markdown-it some.md
script
include:coffee-script(minify=true) include-filter-coffee.coffee
script
include:coffee-script(minify=false) include-filter-coffee.coffee

View File

@@ -0,0 +1,3 @@
<p>
before <![CDATA[inside]]> after</p>

View File

@@ -0,0 +1 @@
p before #[:cdata inside] after

View File

@@ -0,0 +1,7 @@
<html>
<head><style type="text/css">body {
padding: 15px;
}
</style>
</head>
</html>

Some files were not shown because too many files have changed in this diff Show More