FlawOpen

flawopen.com/xss/C-cpp

● CWE-79 · High
FlawOpen Security Research

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.

💡 Plain English Explainer (ELI5)

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 response
printf copies the bytes of a %s argument 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 &amp;, &lt;, &gt;, &quot; and &#39; 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 (%3C for <) 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 &amp; 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-8 so the browser does not guess the encoding, plus X-Content-Type-Options: nosniff and a Content-Security-Policy. A declared charset stops tricks that rely on the browser choosing a different encoding.

Framework mappings

Step-by-Step Attack Flow

Step 1

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 <.

Step 2

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.

Step 3

The browser parses it as markup

The response is declared text/html, so the browser builds an <img> element and fires onerror.

Step 4

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

UNPATCHED FLAW
/* 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;
}
HARDENED SECURE PATCH
/* 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("&amp;", out); break;
        case '<': fputs("&lt;", out); break;
        case '>': fputs("&gt;", out); break;
        case '"': fputs("&quot;", out); break;
        case '\'': fputs("&#39;", 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 &amp; */
    fputs("<a href=\"/search?page=2&amp;q=", stdout);
    write_url_component(stdout, q);
    fputs("\">Next</a>\n", stdout);
    return 0;
}

Engineering & System Hardening Checklist

Sources