This write-up analyzes the Intigriti 0226 challenge and explains how unsafe Markdown rendering, a client-side script re-execution helper, and an exposed JSONP endpoint combine into a CSP bypass and admin session compromise.
├── app
│ ├── app.py
│ ├── Dockerfile
│ ├── favicon.ico
│ ├── requirements.txt
│ ├── static
│ │ ├── css
│ │ │ ├── challenge-style.css
│ │ │ └── style.css
│ │ ├── images
│ │ │ ├── creator.jpg
│ │ │ ├── pattern.svg
│ │ │ └── share.png
│ │ └── js
│ │ └── preview.js
│ └── templates
│ ├── base.html
│ ├── challenge.html
│ ├── dashboard.html
│ ├── index.html
│ ├── login.html
│ ├── post_edit.html
│ ├── post_new.html
│ ├── post_view.html
│ └── register.html
├── bot
│ ├── bot.py
│ ├── Dockerfile
│ └── requirements.txt
├── docker-compose.yml
└── nginx
├── Dockerfile
└── nginx.conf
It is a notes app with features like creating, editing, deleting posts, and a “report to admin” flow.
In app.py, the server renders user content with a custom “Markdown” function:
def render_markdown(content):
html_content = content
html_content = re.sub(r'^### (.+)$', r'<h3>\\1</h3>', html_content, flags=re.MULTILINE)
html_content = re.sub(r'^## (.+)$', r'<h2>\\1</h2>', html_content, flags=re.MULTILINE)
html_content = re.sub(r'^# (.+)$', r'<h1>\\1</h1>', html_content, flags=re.MULTILINE)
html_content = re.sub(r'\\*\\*(.+?)\\*\\*', r'<strong>\\1</strong>', html_content)
html_content = re.sub(r'\\*(.+?)\\*', r'<em>\\1</em>', html_content)
html_content = re.sub(r'\\[(.+?)\\]\\((.+?)\\)', r'<a href="\\2">\\1</a>', html_content)
html_content = html_content.replace('\\n\\n', '</p><p>')
html_content = f'<p>{html_content}</p>'
return html_content
This does not escape HTML. So if a user includes something like <script>...</script> in a note, it will be returned as part of the rendered HTML.

The API endpoint that serves this rendered output is /api/render:
@app.route('/api/render')
def api_render():
post_id = request.args.get('id')
if not post_id:
return jsonify({'error': 'Missing id'}), 400
post = Post.query.get(post_id)
if not post:
return jsonify({'error': 'Not found'}), 404
rendered_html = render_markdown(post.content)
return jsonify({
'id': post.id,
'title': post.title,
'html': rendered_html,
'author': post.author.username,
'rendered_at': time.time()
})
So we have a clear HTML injection primitive in html.
The post preview loads the rendered HTML and inserts it into the DOM using innerHTML:
static/js/preview.js
fetch('/api/render?id=' + postId)
.then(function(response) {
if (!response.ok) throw new Error('Failed to load');
return response.json();
})
.then(function(data) {
const preview = document.getElementById('preview');
preview.innerHTML = data.html;
processContent(preview);
})
.catch(function(error) {
document.getElementById('preview').innerHTML = '<p class="error">Failed to load content.</p>';
});
At this point it looks like classic XSS, but there are two important caveats: