javascript.express.security.audit.xss.direct-response-write.direct-response-write

profile photo of semgrepsemgrep
Author
3,724
Download Count*

Detected directly writing to a Response object from user-defined input. This bypasses any HTML escaping and may expose your application to a Cross-Site-scripting (XSS) vulnerability. Instead, use 'resp.render()' to render safely escaped HTML.

Run Locally

Run in CI

Defintion

rules:
  - id: direct-response-write
    message: Detected directly writing to a Response object from user-defined input.
      This bypasses any HTML escaping and may expose your application to a
      Cross-Site-scripting (XSS) vulnerability. Instead, use 'resp.render()' to
      render safely escaped HTML.
    options:
      interfile: true
    metadata:
      interfile: true
      references:
        - https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
      owasp:
        - A07:2017 - Cross-Site Scripting (XSS)
        - A03:2021 - Injection
      cwe:
        - "CWE-79: Improper Neutralization of Input During Web Page Generation
          ('Cross-site Scripting')"
      category: security
      technology:
        - express
      cwe2022-top25: true
      cwe2021-top25: true
      subcategory:
        - vuln
      likelihood: MEDIUM
      impact: MEDIUM
      confidence: MEDIUM
      license: Commons Clause License Condition v1.0[LGPL-2.1-only]
      vulnerability_class:
        - Cross-Site-Scripting (XSS)
    languages:
      - javascript
      - typescript
    severity: WARNING
    mode: taint
    pattern-sources:
      - patterns:
          - pattern-either:
              - pattern-inside: function ... ($REQ, $RES) {...}
              - pattern-inside: function ... ($REQ, $RES, $NEXT) {...}
              - patterns:
                  - pattern-inside: $APP.$METHOD(..., function $FUNC($REQ, $RES) {...})
                  - metavariable-regex:
                      metavariable: $METHOD
                      regex: ^(get|post|put|head|delete|options)
          - pattern-not-inside: |
              function ... ($REQ, $RES) {
                  ...
                  $RES.$SET('Content-Type', '$TYPE')
              }
          - pattern-not-inside: |
              $APP.$METHOD(..., function $FUNC($REQ, $RES) {
                  ...
                  $RES.$SET('Content-Type', '$TYPE')
              })
          - pattern-not-inside: |
              function ... ($REQ, $RES, $NEXT) {
                  ...
                  $RES.$SET('Content-Type', '$TYPE')
              }
          - pattern-not-inside: |
              function ... ($REQ, $RES) {
                  ...
                  $RES.set('$TYPE')
              }
          - pattern-not-inside: |
              $APP.$METHOD(..., function $FUNC($REQ, $RES) {
                  ...
                  $RES.set('$TYPE')
              })
          - pattern-not-inside: |
              function ... ($REQ, $RES, $NEXT) {
                  ...
                  $RES.set('$TYPE')
              }
          - pattern-either:
              - pattern: $REQ.query
              - pattern: $REQ.body
              - pattern: $REQ.params
      - patterns:
          - pattern-either:
              - pattern-inside: |
                  ({ $REQ }: Request,$RES: Response, $NEXT: NextFunction) =>
                  {...}
              - pattern-inside: |
                  ({ $REQ }: Request,$RES: Response) => {...}
          - pattern-not-inside: |
              ({ $REQ }: Request,$RES: Response, $NEXT: NextFunction) =>
              {
                  ...
                  $RES.$SET('Content-Type', '$TYPE')
              }
          - pattern-not-inside: |
              ({ $REQ }: Request,$RES: Response) => {
                  ...
                  $RES.$SET('Content-Type', '$TYPE')
              }
          - pattern-not-inside: |
              ({ $REQ }: Request,$RES: Response, $NEXT: NextFunction) =>
              {
                  ...
                  $RES.set('$TYPE')
              }
          - focus-metavariable: $REQ
          - pattern-either:
              - pattern: params
              - pattern: query
              - pattern: body
    pattern-sinks:
      - patterns:
          - pattern-inside: function ... (..., $RES,...) {...}
          - pattern-either:
              - pattern: $RES.write($ARG)
              - pattern: $RES.send($ARG)
          - pattern-not: $RES. ... .set('...'). ... .send($ARG)
          - pattern-not: $RES. ... .type('...'). ... .send($ARG)
          - focus-metavariable: $ARG
    pattern-sanitizers:
      - patterns:
          - pattern-either:
              - pattern-inside: |
                  import $S from "underscore.string"
                  ...
              - pattern-inside: |
                  import * as $S from "underscore.string"
                  ...
              - pattern-inside: |
                  import $S from "underscore.string"
                  ...
              - pattern-inside: |
                  $S = require("underscore.string")
                  ...
          - pattern-either:
              - pattern: $S.escapeHTML(...)
      - patterns:
          - pattern-either:
              - pattern-inside: |
                  import $S from "dompurify"
                  ...
              - pattern-inside: |
                  import { ..., $S,... } from "dompurify"
                  ...
              - pattern-inside: |
                  import * as $S from "dompurify"
                  ...
              - pattern-inside: |
                  $S = require("dompurify")
                  ...
              - pattern-inside: |
                  import $S from "isomorphic-dompurify"
                  ...
              - pattern-inside: |
                  import * as $S from "isomorphic-dompurify"
                  ...
              - pattern-inside: |
                  $S = require("isomorphic-dompurify")
                  ...
          - pattern-either:
              - patterns:
                  - pattern-inside: |
                      $VALUE = $S(...)
                      ...
                  - pattern: $VALUE.sanitize(...)
              - patterns:
                  - pattern-inside: |
                      $VALUE = $S.sanitize
                      ...
                  - pattern: $S(...)
              - pattern: $S.sanitize(...)
              - pattern: $S(...)
      - patterns:
          - pattern-either:
              - pattern-inside: |
                  import $S from 'xss';
                  ...
              - pattern-inside: |
                  import * as $S from 'xss';
                  ...
              - pattern-inside: |
                  $S = require("xss")
                  ...
          - pattern: $S(...)
      - patterns:
          - pattern-either:
              - pattern-inside: |
                  import $S from 'sanitize-html';
                  ...
              - pattern-inside: |
                  import * as $S from "sanitize-html";
                  ...
              - pattern-inside: |
                  $S = require("sanitize-html")
                  ...
          - pattern: $S(...)
      - patterns:
          - pattern-either:
              - pattern-inside: |
                  $S = new Remarkable()
                  ...
          - pattern: $S.render(...)
      - patterns:
          - pattern-either:
              - pattern-inside: |
                  import $S from 'express-xss-sanitizer';
                  ...
              - pattern-inside: |
                  import * as $S from "express-xss-sanitizer";
                  ...
              - pattern-inside: |
                  const { ..., $S, ... } = require('express-xss-sanitizer');
                  ...
              - pattern-inside: |
                  var { ..., $S, ... } = require('express-xss-sanitizer');
                  ...
              - pattern-inside: |
                  let { ...,$S,... } = require('express-xss-sanitizer');
                  ...
              - pattern-inside: |
                  $S = require("express-xss-sanitizer")
                  ...
          - pattern: $S(...)
      - patterns:
          - pattern: $RES. ... .type('$F'). ... .send(...)
          - metavariable-regex:
              metavariable: $F
              regex: (?!.*text/html)
      - patterns:
          - pattern-inside: |
              $X = [...];
              ...
          - pattern: |
              if(<... !$X.includes($SOURCE)...>) {
                  ...
                  return ...
              }
              ...
          - pattern: $SOURCE

Examples

direct-response-write.js


const express = require('express')
const router = express.Router()
var xss = require("xss");

import {
    AdminUpdateUserAttributesCommand,
    CognitoIdentityProviderClient,
} from "@aws-sdk/client-cognito-identity-provider";


router.get('/greeting', (req, res) => {
    const { name } = req.query;
    // ruleid: direct-response-write
    res.send('<h1> Hello :' + name + "</h1>")
})

//template handle escaping
router.get('/greet-template', (req, res) => {
    name = req.query.name
    // ok: direct-response-write
    res.render('index', { user_name: name });
})

//template handle escaping
router.get('/greet-template', (req, res) => {
    a = req.query.name
    // ok: direct-response-write
    res.send('<h1> Hello :' + xss(a) + "</h1>")
})


module.exports = router


app.get('/', function (req, res) {
    var user = req.query.name;

    msg = "Hi " + user
    // ruleid: direct-response-write
    res.send('Response</br>' + msg);
});


var msg = '';
app.get('/3', function (req, res) {
    var user = req.query.name;

    msg = "Hi " + user
    // ruleid: direct-response-write
    res.send('Response</br>' + msg);
});

app.get('/2', function (req, res) {
    var user = { user: req.query.name };
    // ruleid: direct-response-write
    res.send('Response</br>' + user.name);
});


app.get('/4', function (req, res) {
    var user = req.query.name;
    var header = "<html>";
    var msg = 'Hi ' + user;
    var footer = "</html>";
    var output = header + msg + footer;
    // ruleid: direct-response-write
    res.send(output);
});

app.get('/4', function (req, res) {
    var user = req.query.name;
    var header = "<html>";
    var msg = 'Hi ' + user;
    var footer = "</html>";
    var output = header + msg + footer;
    // ok: direct-response-write
    res.type('xml').set('Content-Length', Buffer.byteLength(xml)).send(xml);
});

var express = require('express');
var app = express();
app.get('/', function (req, res) {
    var resp = req.query.name;
    // ruleid: direct-response-write
    res.send('Response</br>' + resp);
});
app.get('/3', function (req, res) {
    var resp = req.query.name;
    // ruleid: direct-response-write
    res.write('Response</br>' + resp);
});

app.get('/3', function (req, res) {
    var resp = req.foo;
    var x = 1;
    // ok: direct-response-write
    res.write('Response</br>' + resp);
});

app.get('/xss', function (req, res) {
    var html = "ASadad" + req.query.name + "Asdadads"
    // ruleid: direct-response-write
    res.write('Response</br>' + html);
});
app.get('/xss', function (req, res) {
    // ruleid: direct-response-write
    res.write('Response</br>' + req.query('doo'));
});
app.get('/xss', function (req, res) {
    // ok: direct-response-write
    res.set('Content-Type','text/plain')
    res.write('Response</br>' + req.query.name);
});

app.get('/noxss', function (req, res) {
    var resp = req.query.name;
    // ok: direct-response-write
    res.write('Response</br>');
});

app.get('/noxs2s', function (req, res) {
    var resp = req.query.name;
    // ruleid: direct-response-write
    res.write('Response</br>' + resp);
});

app.get('/xss', function (req, res) {
    var resp = req.query.name;
    var html = "ASadad" + resp + "Asdadads"
    // ruleid: direct-response-write
    res.write('Response</br>' + html);
});

// For https://github.com/returntocorp/semgrep-rules/issues/2872
app.post(
    "/:id",
    async (req, res, next) => {
        const userId = req.params?.id;

        if (user.email !== req.body.email) {
            const command = new AdminUpdateUserAttributesCommand({
                Username: user.cognitoUserId,
                UserPoolId: process.env.COGNITO_USER_POOL_ID,
                UserAttributes: [
                    {
                        Name: "email",
                        Value: req.body.email,
                    }
                ],
            });

            // ok: direct-response-write
            await client.send(command);
        }

        res.status(200).send();
    }
);


app.listen(8000);