-
Notifications
You must be signed in to change notification settings - Fork 306
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
exploit prevention blocking - missing safeguards for writes after blocks
- Loading branch information
Showing
8 changed files
with
347 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
'use strict' | ||
|
||
const getPort = require('get-port') | ||
const { createSandbox, FakeAgent, spawnProc } = require('../helpers') | ||
const path = require('path') | ||
const Axios = require('axios') | ||
const { assert } = require('chai') | ||
|
||
describe('RASP', () => { | ||
let axios, sandbox, cwd, appPort, appFile, agent, proc, stdioHandler | ||
|
||
function stdOutputHandler (data) { | ||
stdioHandler && stdioHandler(data) | ||
} | ||
|
||
before(async () => { | ||
sandbox = await createSandbox(['express']) | ||
appPort = await getPort() | ||
cwd = sandbox.folder | ||
appFile = path.join(cwd, 'appsec/rasp/index.js') | ||
axios = Axios.create({ | ||
baseURL: `http://localhost:${appPort}` | ||
}) | ||
}) | ||
|
||
beforeEach(async () => { | ||
agent = await new FakeAgent().start() | ||
proc = await spawnProc(appFile, { | ||
cwd, | ||
env: { | ||
AGENT_PORT: agent.port, | ||
APP_PORT: appPort, | ||
DD_APPSEC_ENABLED: true, | ||
DD_APPSEC_RASP_ENABLED: true, | ||
DD_APPSEC_RULES: path.join(cwd, 'appsec/rasp/rasp_rules.json') | ||
} | ||
}, stdOutputHandler, stdOutputHandler) | ||
}) | ||
|
||
afterEach(async () => { | ||
proc.kill() | ||
await agent.stop() | ||
}) | ||
|
||
after(async () => { | ||
await sandbox.remove() | ||
}) | ||
|
||
async function testNotCrashedAfterBlocking (path) { | ||
let hasOutput = false | ||
stdioHandler = () => { | ||
hasOutput = true | ||
} | ||
try { | ||
await axios.get(`${path}?host=ifconfig.pro`) | ||
assert.fail('Request should have failed') | ||
} catch (e) { | ||
if (!e.response) { | ||
throw e | ||
} | ||
assert.strictEqual(e.response.status, 403) | ||
} | ||
return new Promise((resolve, reject) => { | ||
setTimeout(() => { | ||
if (hasOutput) { | ||
reject(new Error('Unexpected output in stdout/stderr after blocking request')) | ||
} else { | ||
resolve() | ||
} | ||
}, 50) | ||
}) | ||
} | ||
|
||
describe('ssrf', () => { | ||
it('should block when error is unhandled', async () => { | ||
try { | ||
await axios.get('/ssrf/http/unhandled-error?host=ifconfig.pro') | ||
assert.fail('Request should have failed') | ||
} catch (e) { | ||
assert.strictEqual(e.response.status, 403) | ||
} | ||
}) | ||
|
||
// Not implemented yet | ||
it('should not crash the app when app send data after blocking', () => { | ||
return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-A') | ||
}) | ||
|
||
it('should not crash the app when app stream data after blocking', () => { | ||
return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-B') | ||
}) | ||
|
||
it('should not crash the app when setHeader, writeHead or end after blocking', () => { | ||
return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-C') | ||
}) | ||
|
||
it('should not crash the app when appendHeader, flushHeaders, removeHeader after blocking', () => { | ||
return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-D') | ||
}) | ||
|
||
it('should not crash the app when writeContinue after blocking', () => { | ||
return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-E') | ||
}) | ||
|
||
it('should not crash the app when writeProcessing after blocking', () => { | ||
return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-F') | ||
}) | ||
|
||
it('should not crash the app when writeEarlyHints after blocking', () => { | ||
return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-G') | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
'use strict' | ||
|
||
const path = require('path') | ||
const fs = require('fs') | ||
require('dd-trace').init() | ||
|
||
const http = require('https') | ||
const express = require('express') | ||
|
||
const app = express() | ||
const port = process.env.APP_PORT || 3000 | ||
|
||
app.get('/ping', (req, res) => { | ||
res.end('pong') | ||
}) | ||
|
||
function makeOutgoingRequestAndCbAfterTimeout (req, res, cb) { | ||
let finished = false | ||
setTimeout(() => { | ||
if (!finished && cb) { | ||
cb() | ||
} | ||
}, 10) | ||
|
||
http.get(`https://${req.query.host}`, () => { | ||
finished = true | ||
res.send('end') | ||
}) | ||
} | ||
|
||
app.get('/ssrf/http/unhandled-error', (req, res) => { | ||
makeOutgoingRequestAndCbAfterTimeout(req, res) | ||
}) | ||
app.get('/ssrf/http/unhandled-async-write-A', (req, res) => { | ||
makeOutgoingRequestAndCbAfterTimeout(req, res, () => { | ||
res.send('Late end') | ||
}) | ||
}) | ||
|
||
app.get('/ssrf/http/unhandled-async-write-B', (req, res) => { | ||
makeOutgoingRequestAndCbAfterTimeout(req, res, () => { | ||
streamFile(res) | ||
}) | ||
}) | ||
|
||
app.get('/ssrf/http/unhandled-async-write-C', (req, res) => { | ||
makeOutgoingRequestAndCbAfterTimeout(req, res, () => { | ||
res.setHeader('key', 'value') | ||
res.writeHead(200, 'OK', ['key2', 'value2']) | ||
res.write('test\n') | ||
res.end('end') | ||
}) | ||
}) | ||
|
||
app.get('/ssrf/http/unhandled-async-write-D', (req, res) => { | ||
makeOutgoingRequestAndCbAfterTimeout(req, res, () => { | ||
res.setHeader('key', 'value') | ||
res.appendHeader('key2', 'value2') | ||
res.removeHeader('key') | ||
res.flushHeaders() | ||
res.end('end') | ||
}) | ||
}) | ||
|
||
app.get('/ssrf/http/unhandled-async-write-E', (req, res) => { | ||
makeOutgoingRequestAndCbAfterTimeout(req, res, () => { | ||
res.writeContinue() | ||
res.end() | ||
}) | ||
}) | ||
|
||
app.get('/ssrf/http/unhandled-async-write-F', (req, res) => { | ||
makeOutgoingRequestAndCbAfterTimeout(req, res, () => { | ||
res.writeProcessing() | ||
res.end() | ||
}) | ||
}) | ||
|
||
app.get('/ssrf/http/unhandled-async-write-G', (req, res) => { | ||
makeOutgoingRequestAndCbAfterTimeout(req, res, () => { | ||
const earlyHintsLink = '</styles.css>; rel=preload; as=style' | ||
res.writeEarlyHints({ | ||
link: earlyHintsLink | ||
}) | ||
res.end() | ||
}) | ||
}) | ||
|
||
function streamFile (res) { | ||
const stream = fs.createReadStream(path.join(__dirname, 'streamtest.txt'), { encoding: 'utf8' }) | ||
stream.pipe(res, { end: false }) | ||
stream.on('end', () => res.end('end')) | ||
} | ||
|
||
app.listen(port, () => { | ||
process.send({ port }) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
{ | ||
"version": "2.2", | ||
"metadata": { | ||
"rules_version": "1.99.0" | ||
}, | ||
"rules": [ | ||
{ | ||
"id": "test-rule-id-2", | ||
"name": "Server-side request forgery exploit", | ||
"enabled": true, | ||
"tags": { | ||
"type": "ssrf", | ||
"category": "vulnerability_trigger", | ||
"cwe": "918", | ||
"capec": "1000/225/115/664", | ||
"confidence": "0", | ||
"module": "rasp" | ||
}, | ||
"conditions": [ | ||
{ | ||
"parameters": { | ||
"resource": [ | ||
{ | ||
"address": "server.io.net.url" | ||
} | ||
], | ||
"params": [ | ||
{ | ||
"address": "server.request.query" | ||
}, | ||
{ | ||
"address": "server.request.body" | ||
}, | ||
{ | ||
"address": "server.request.path_params" | ||
}, | ||
{ | ||
"address": "grpc.server.request.message" | ||
}, | ||
{ | ||
"address": "graphql.server.all_resolvers" | ||
}, | ||
{ | ||
"address": "graphql.server.resolver" | ||
} | ||
] | ||
}, | ||
"operator": "ssrf_detector" | ||
} | ||
], | ||
"transformers": [], | ||
"on_match": [ | ||
"block", | ||
"stack_trace" | ||
] | ||
} | ||
] | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.