flawopen.com/xss/C-cpp
Cross-Site Scripting (XSS) in C/C++ (CGI, Crow)
Writing a decoded query parameter into a CGI response with printf("%s") or a Crow {{{ }}} tag lets a visitor's text run as JavaScript in other users' browsers (CWE-79). Native web code has no template engine escaping for it by default: the two encoders every C or C++ handler needs, the headers that go with them, and where Crow escapes for you.
Imagine a bus terminal printing passengers’ typed destinations onto its official departure board. A fixed printf("%s") copies the bytes, but if a passenger’s text is inserted into HTML without context-specific encoding, the browser treats characters such as < and quotes as board structure rather than passenger text. The CGI program first decodes QUERY_STRING, so percent-encoding does not keep those characters harmless. Encode separately for HTML text, a quoted attribute, and a URL component. The text/html response tells the browser to parse the result as a page.
Core Concepts & Subsystem Terms
printf("%s") into an HTML responseprintfcopies the bytes of a%sargument verbatim. In an HTML response,<,"and&from the request become markup, and there is no framework layer to escape them for you.HTML entity encoding- Replace
&,<,>,"and'with&,<,>,"and'before writing a value into element content or a double-quoted attribute. Encode in a single pass over the input, so an&you have just written is never encoded twice. Percent-encoding for URL parameters- A value inside a URL query string must be percent-encoded (
%3Cfor<) so it cannot end the parameter or the attribute. HTML-escaping alone is not enough there, and the literal&separating parameters must itself be written as&inside an HTML attribute. Crow mustache {{ }} vs {{{ }}}- Crow's mustache templates HTML-escape
{{name}}and insert{{{name}}}(or{{& name}}) raw. Use the triple form only for content you generated or sanitized yourself. Content-Type and nosniff headers- Always send
Content-Type: text/html; charset=utf-8so the browser does not guess the encoding, plusX-Content-Type-Options: nosniffand a Content-Security-Policy. A declared charset stops tricks that rely on the browser choosing a different encoding.
Framework mappings
- CWE reference for the weakness described in this guide: CWE-79
- OWASP Top 10:2025 — A05:2025 Injection
Step-by-Step Attack Flow
Input reaches the CGI program
The web server passes the query string in the QUERY_STRING environment variable; query_param() percent-decodes q, so %3C becomes a literal <.
printf copies it into the page
The %s specifier writes the decoded bytes into both the heading and the link's href. With the benign test value <img src=x onerror=alert(1)>, the heading contains a real tag; a " in the value would end the href attribute.
The browser parses it as markup
The response is declared text/html, so the browser builds an <img> element and fires onerror.
The script runs as the site
The code executes with the site's origin: it can read the DOM, call the site's endpoints with the victim's cookies and change what the user sees.
Source Code: Flaw vs. Secure Implementation
/* search.c: request data is written into the HTML without encoding */
#include <stdio.h>
#include <stdlib.h>
char *query_param(const char *name);
int main(void)
{
const char *q = query_param("q");
/* no charset, no nosniff, no CSP */
printf("Content-Type: text/html\r\n\r\n");
/* %s copies the decoded query into the element as markup */
printf("<h1>Results for %s</h1>\n", q ? q : "");
/* the query is placed in a URL without percent-encoding */
printf("<a href=\"/search?page=2&q=%s\">Next</a>\n", q ? q : "");
return 0;
}
/* search.c: every value is encoded for the context it is written into */
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
char *query_param(const char *name);
/* HTML encoder for element content and double-quoted attributes */
static void write_html(FILE *out, const char *s)
{
for (; s && *s; s++) {
switch (*s) {
case '&': fputs("&", out); break;
case '<': fputs("<", out); break;
case '>': fputs(">", out); break;
case '"': fputs(""", out); break;
case '\'': fputs("'", out); break;
default: fputc(*s, out);
}
}
}
/* percent-encoder for a value inside a URL query string */
static void write_url_component(FILE *out, const char *s)
{
for (; s && *s; s++) {
unsigned char c = (unsigned char)*s;
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~')
fputc(c, out);
else
fprintf(out, "%%%02X", c);
}
}
int main(void)
{
const char *q = query_param("q");
/* explicit charset, nosniff and a CSP that blocks inline script */
printf("Content-Type: text/html; charset=utf-8\r\n"
"X-Content-Type-Options: nosniff\r\n"
"Content-Security-Policy: default-src 'self'; script-src 'self'\r\n\r\n");
/* the query is HTML-encoded before it reaches the heading */
fputs("<h1>Results for ", stdout);
write_html(stdout, q);
fputs("</h1>\n", stdout);
/* the query is percent-encoded, and & is written as & */
fputs("<a href=\"/search?page=2&q=", stdout);
write_url_component(stdout, q);
fputs("\">Next</a>\n", stdout);
return 0;
}
Engineering & System Hardening Checklist
- ✓Flag every
printf,fprintf,fputsor stream<<that writes request-derived data into an HTML response without passing through the HTML or URL encoder; CodeQL'scpp/cgi-xssquery covers the CGI case. - ✓Keep one HTML encoder and one URL-component encoder in a shared module, unit-test them against
&<>"'and non-ASCII input, and ban ad-hoc escaping. - ✓In Crow templates, use
{{name}}for all request data and grep CI for{{{and{{&. - ✓Always send
Content-Type: text/html; charset=utf-8,X-Content-Type-Options: nosniffand a Content-Security-Policy that disallows inline script. - ✓Set session cookies with
HttpOnly,SecureandSameSite=Laxso an injected script cannot read them directly.