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

Author
3,724
Download Count*
License
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.
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]
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);
Short Link: https://sg.run/vzGl