Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exploit prevention ssrf blocking #4372

Merged
merged 52 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
3ddd107
http client instrumentation for AbortController
uurien Jun 4, 2024
9b5ea83
exploit prevention blocking - missing safeguards for writes after blocks
uurien Jun 6, 2024
c11cb22
Use responses Set to prevent crashing when tracer blocks
uurien Jun 13, 2024
619273e
Rename internal method name
uurien Jun 13, 2024
36e0a28
Use abortController.signal.reason instead of abortData object
uurien Jun 13, 2024
054ca44
Fix tests
uurien Jun 13, 2024
5e5658f
Hacks to support node 16.0
uurien Jun 13, 2024
a4142bf
lint
uurien Jun 13, 2024
96f208c
Add test case
uurien Jun 14, 2024
c6837a9
Remove unused code from a test
uurien Jun 14, 2024
6e969e6
Add tests using axios
uurien Jun 17, 2024
8c1612e
Do not block when it is not using express
uurien Jun 17, 2024
db3935f
Merge branch 'master' into ugaitz/rasp-blocking
uurien Jun 17, 2024
05c5371
Fix stack traces
uurien Jun 17, 2024
5b94ef6
Improve axios support
uurien Jun 17, 2024
70fd11c
Small changes
uurien Jun 17, 2024
2681a85
Change a bit unhandled exception approach
uurien Jun 20, 2024
5323576
support blocking unhandled errors with --abort-on-uncaught-exception
uurien Jun 21, 2024
cb6fd05
Merge branch 'master' into ugaitz/rasp-blocking
uurien Jun 24, 2024
52a7f2c
Revert "support blocking unhandled errors with --abort-on-uncaught-ex…
uurien Jun 25, 2024
1c14eab
Improve integration tests
uurien Jun 25, 2024
4a29cc7
Merge branch 'master' into ugaitz/rasp-blocking
uurien Jun 25, 2024
20209f5
Small fixes
uurien Jun 25, 2024
3e88312
Try to handle aborterror when app has its own uncaughtExceptionCaptur…
uurien Jun 27, 2024
693d8be
Typo
uurien Jun 27, 2024
32e3627
Use correct axios in the test
uurien Jun 27, 2024
54991b5
Merge branch 'master' into ugaitz/rasp-blocking
uurien Jun 27, 2024
14d5283
Fix test
uurien Jun 27, 2024
519751c
Compatibility with domain
uurien Jun 27, 2024
68a0009
Small changes
uurien Jun 27, 2024
268de00
Small change
uurien Jun 27, 2024
3288410
Lint fix
uurien Jun 27, 2024
951898d
Enable appsec integration tests
uurien Jun 28, 2024
194ec41
Change blocking approach
uurien Jun 28, 2024
b9a5d4d
Merge branch 'master' into ugaitz/rasp-blocking
uurien Jul 1, 2024
be9d2f8
Apply PR suggestions
uurien Jul 3, 2024
6274362
Lint fix
uurien Jul 3, 2024
0a462d9
Update packages/dd-trace/src/appsec/rasp.js
uurien Jul 3, 2024
f2b7253
Rename AbortError to DatadogRaspAbortError
uurien Jul 3, 2024
6487c9b
Suggestion from PR
uurien Jul 3, 2024
e9aa877
Merge branch 'master' into ugaitz/rasp-blocking
uurien Jul 4, 2024
53b4a40
Merge branch 'master' into ugaitz/rasp-blocking
uurien Jul 12, 2024
c4385bc
Update test integration appsec timeout
uurien Jul 12, 2024
af37d5b
Merge branch 'master' into ugaitz/rasp-blocking
uurien Jul 17, 2024
d3382da
Merge branch 'master' into ugaitz/rasp-blocking
uurien Jul 18, 2024
2712156
Merge branch 'master' into ugaitz/rasp-blocking
uurien Jul 19, 2024
8870642
Improvements from PR comments
uurien Jul 19, 2024
231f4ee
Small change proposed in the PR
uurien Jul 19, 2024
afc4904
Fix lint
uurien Jul 19, 2024
fb6bf3f
Small fix
uurien Jul 22, 2024
bed2f36
Merge branch 'master' into ugaitz/rasp-blocking
uurien Jul 22, 2024
15f1d33
Fix typo
uurien Jul 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/appsec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,13 @@ jobs:
- uses: ./.github/actions/node/latest
- run: yarn test:appsec:plugins:ci
- uses: codecov/codecov-action@v3

integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: yarn install
- uses: ./.github/actions/node/oldest
- run: yarn test:integration:appsec
- uses: ./.github/actions/node/latest
- run: yarn test:integration:appsec
342 changes: 342 additions & 0 deletions integration-tests/appsec/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,342 @@
'use strict'

const getPort = require('get-port')
const path = require('path')
const Axios = require('axios')
const { assert } = require('chai')
const { createSandbox, FakeAgent, spawnProc } = require('../helpers')

describe('RASP', () => {
let axios, sandbox, cwd, appPort, appFile, agent, proc, stdioHandler

function stdOutputHandler (data) {
stdioHandler && stdioHandler(data)
}

before(async () => {
sandbox = await createSandbox(['express', 'axios'])
appPort = await getPort()
cwd = sandbox.folder
appFile = path.join(cwd, 'appsec/rasp/index.js')
axios = Axios.create({
baseURL: `http://localhost:${appPort}`
})
})

after(async () => {
await sandbox.remove()
})

function startServer (abortOnUncaughtException) {
beforeEach(async () => {
let execArgv = process.execArgv
if (abortOnUncaughtException) {
execArgv = ['--abort-on-uncaught-exception', ...execArgv]
}
agent = await new FakeAgent().start()
proc = await spawnProc(appFile, {
cwd,
execArgv,
env: {
DD_TRACE_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()
})
}

async function assertExploitDetected () {
await agent.assertMessageReceived(({ headers, payload }) => {
assert.property(payload[0][0].meta, '_dd.appsec.json')
assert.include(payload[0][0].meta['_dd.appsec.json'], '"test-rule-id-2"')
})
}

describe('--abort-on-uncaught-exception is not configured', () => {
startServer(false)

async function testNotCrashedAfterBlocking (path) {
let hasOutput = false
stdioHandler = () => {
hasOutput = true
}

try {
await axios.get(`${path}?host=localhost/ifconfig.pro`)

assert.fail('Request should have failed')
} catch (e) {
if (!e.response) {
throw e
}

assert.strictEqual(e.response.status, 403)
await assertExploitDetected()
}

return new Promise((resolve, reject) => {
setTimeout(() => {
if (hasOutput) {
reject(new Error('Unexpected output in stdout/stderr after blocking request'))
} else {
resolve()
}
}, 50)
})
}

async function testCustomErrorHandlerIsNotExecuted (path) {
let hasOutput = false
try {
stdioHandler = () => {
hasOutput = true
}

await axios.get(`${path}?host=localhost/ifconfig.pro`)

assert.fail('Request should have failed')
} catch (e) {
if (!e.response) {
throw e
}

assert.strictEqual(e.response.status, 403)
await assertExploitDetected()

return new Promise((resolve, reject) => {
setTimeout(() => {
if (hasOutput) {
reject(new Error('uncaughtExceptionCaptureCallback executed'))
} else {
resolve()
}
}, 10)
})
}
}

async function testAppCrashesAsExpected () {
let hasOutput = false
stdioHandler = () => {
hasOutput = true
}

try {
await axios.get('/crash')
} catch (e) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (hasOutput) {
resolve()
} else {
reject(new Error('Output expected after crash'))
}
}, 50)
})
}

assert.fail('Request should have failed')
}

describe('ssrf', () => {
it('should crash when error is not an AbortError', async () => {
await testAppCrashesAsExpected()
})

it('should not crash when customer has his own setUncaughtExceptionCaptureCallback', async () => {
let hasOutput = false
stdioHandler = () => {
hasOutput = true
}

try {
await axios.get('/crash-and-recovery-A')
} catch (e) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (hasOutput) {
reject(new Error('Unexpected output in stdout/stderr after blocking request'))
} else {
resolve()
}
}, 50)
})
}

assert.fail('Request should have failed')
})

it('should not crash when customer has his own uncaughtException', async () => {
let hasOutput = false
stdioHandler = () => {
hasOutput = true
}

try {
await axios.get('/crash-and-recovery-B')
} catch (e) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (hasOutput) {
reject(new Error('Unexpected output in stdout/stderr after blocking request'))
} else {
resolve()
}
}, 50)
})
}

assert.fail('Request should have failed')
})

it('should block manually', async () => {
let response

try {
response = await axios.get('/ssrf/http/manual-blocking?host=localhost/ifconfig.pro')
} catch (e) {
if (!e.response) {
throw e
}
response = e.response
assert.strictEqual(response.status, 418)
return await assertExploitDetected()
}

assert.fail('Request should have failed')
})

it('should block in a domain', async () => {
let response

try {
response = await axios.get('/ssrf/http/should-block-in-domain?host=localhost/ifconfig.pro')
} catch (e) {
if (!e.response) {
throw e
}
response = e.response
assert.strictEqual(response.status, 403)
return await assertExploitDetected()
}

assert.fail('Request should have failed')
})

it('should crash as expected after block in domain request', async () => {
try {
await axios.get('/ssrf/http/should-block-in-domain?host=localhost/ifconfig.pro')
} catch (e) {
return await testAppCrashesAsExpected()
}

assert.fail('Request should have failed')
})

it('should block when error is unhandled', async () => {
try {
await axios.get('/ssrf/http/unhandled-error?host=localhost/ifconfig.pro')
} catch (e) {
if (!e.response) {
throw e
}

assert.strictEqual(e.response.status, 403)
return await assertExploitDetected()
}

assert.fail('Request should have failed')
})

it('should crash as expected after a requiest block when error is unhandled', async () => {
try {
await axios.get('/ssrf/http/unhandled-error?host=localhost/ifconfig.pro')
} catch (e) {
return await testAppCrashesAsExpected()
}

assert.fail('Request should have failed')
})

it('should not execute custom uncaughtExceptionCaptureCallback when it is blocked', async () => {
return testCustomErrorHandlerIsNotExecuted('/ssrf/http/custom-uncaught-exception-capture-callback')
})

it('should not execute custom uncaughtException listener', async () => {
return testCustomErrorHandlerIsNotExecuted('/ssrf/http/custom-uncaughtException-listener')
})

it('should not crash when app send data after blocking', () => {
return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-A')
})

it('should not crash when app stream data after blocking', () => {
return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-B')
})

it('should not crash when setHeader, writeHead or end after blocking', () => {
return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-C')
})

it('should not crash when appendHeader, flushHeaders, removeHeader after blocking', () => {
return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-D')
})

it('should not crash when writeContinue after blocking', () => {
return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-E')
})

it('should not crash when writeProcessing after blocking', () => {
return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-F')
})

it('should not crash when writeEarlyHints after blocking', () => {
return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-G')
})

it('should not crash when res.json after blocking', () => {
return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-H')
})

it('should not crash when is blocked using axios', () => {
return testNotCrashedAfterBlocking('/ssrf/http/unhandled-axios')
})

it('should not crash when is blocked with unhandled rejection', () => {
return testNotCrashedAfterBlocking('/ssrf/http/unhandled-promise')
})
})
})

describe('--abort-on-uncaught-exception is configured', () => {
startServer(true)

describe('ssrf', () => {
it('should not block', async () => {
let response

try {
response = await axios.get('/ssrf/http/manual-blocking?host=localhost/ifconfig.pro')
} catch (e) {
if (!e.response) {
throw e
}
response = e.response
}

// not blocked
assert.notEqual(response.status, 418)
assert.notEqual(response.status, 403)
await assertExploitDetected()
})
})
})
})
Loading
Loading