Skip to content

Commit

Permalink
Support url.parse, url.URL.parse and new url.URL for taint tracking
Browse files Browse the repository at this point in the history
  • Loading branch information
uurien committed Oct 29, 2024
1 parent 1c0958e commit 1ea43eb
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 2 deletions.
13 changes: 12 additions & 1 deletion .github/workflows/plugins.yml
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,17 @@ jobs:
uses: ./.github/actions/testagent/logs
- uses: codecov/codecov-action@v3

url:
strategy:
matrix:
node-version: ['18', '20', 'latest']
runs-on: ubuntu-latest
env:
PLUGINS: url
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/plugins/test

http2:
runs-on: ubuntu-latest
env:
Expand Down Expand Up @@ -578,7 +589,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/plugins/test

mariadb:
runs-on: ubuntu-latest
services:
Expand Down
2 changes: 2 additions & 0 deletions packages/datadog-instrumentations/src/helpers/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ module.exports = {
'node:http2': () => require('../http2'),
'node:https': () => require('../http'),
'node:net': () => require('../net'),
'node:url': () => require('../url'),
nyc: () => require('../nyc'),
oracledb: () => require('../oracledb'),
openai: () => require('../openai'),
Expand All @@ -115,6 +116,7 @@ module.exports = {
sharedb: () => require('../sharedb'),
tedious: () => require('../tedious'),
undici: () => require('../undici'),
url: () => require('../url'),
vitest: { esmFirst: true, fn: () => require('../vitest') },
when: () => require('../when'),
winston: () => require('../winston'),
Expand Down
85 changes: 85 additions & 0 deletions packages/datadog-instrumentations/src/url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use strict'

const { addHook, channel } = require('./helpers/instrument')
const shimmer = require('../../datadog-shimmer')
const names = ['url', 'node:url']

const parseFinishedChannel = channel('datadog:url:parse:finish')
const urlGetterChannel = channel('datadog:url:getter:finish')
const instrumentedGetters = ['host', 'origin', 'hostname']

addHook({ name: names }, function (url) {
shimmer.wrap(url, 'parse', (parse) => {
return function wrappedParse () {
const parsedValue = parse.apply(this, arguments)
if (!parseFinishedChannel.hasSubscribers) return parsedValue

parseFinishedChannel.publish({
input: arguments[0],
parsed: parsedValue,
isURL: false
})

return parsedValue
}
})

const URLPrototype = url.URL.prototype.constructor.prototype
instrumentedGetters.forEach(property => {
const originalDescriptor = Object.getOwnPropertyDescriptor(URLPrototype, property)

if (originalDescriptor?.get) {
const newDescriptor = {
...originalDescriptor,
get: function () {
const result = originalDescriptor.get.apply(this, arguments)
if (!urlGetterChannel.hasSubscribers) return result

const context = { urlObject: this, result, property }
urlGetterChannel.publish(context)

return context.result
}
}

Object.defineProperty(URLPrototype, property, newDescriptor)
}
})

shimmer.wrap(url, 'URL', (URL) => {
return class extends URL {
constructor () {
super(...arguments)

if (!parseFinishedChannel.hasSubscribers) return

parseFinishedChannel.publish({
input: arguments[0],
base: arguments[1],
parsed: this,
isURL: true
})
}
}
})

if (url.URL.parse) {
shimmer.wrap(url.URL, 'parse', (parse) => {
return function wrappedParse () {
const parsedValue = parse.apply(this, arguments)
if (!parseFinishedChannel.hasSubscribers) return parsedValue

parseFinishedChannel.publish({
input: arguments[0],
base: arguments[1],
parsed: parsedValue,
isURL: true
})

return parsedValue
}
})
}

return url
})
114 changes: 114 additions & 0 deletions packages/datadog-instrumentations/test/url.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
'use strict'

const agent = require('../../dd-trace/test/plugins/agent')
const { channel } = require('../src/helpers/instrument')
const names = ['url', 'node:url']

names.forEach(name => {
describe(name, () => {
const url = require(name)
const parseFinishedChannel = channel('datadog:url:parse:finish')
const urlGetterChannel = channel('datadog:url:getter:finish')
let parseFinishedChannelCb, urlGetterChannelCb

before(async () => {
await agent.load('url')
})

after(() => {
return agent.close()
})

beforeEach(() => {
parseFinishedChannelCb = sinon.stub()
urlGetterChannelCb = sinon.stub()
parseFinishedChannel.subscribe(parseFinishedChannelCb)
urlGetterChannel.subscribe(urlGetterChannelCb)
})

afterEach(() => {
parseFinishedChannel.unsubscribe(parseFinishedChannelCb)
urlGetterChannel.unsubscribe(urlGetterChannelCb)
})

describe('url.parse', () => {
it('should publish', () => {
// eslint-disable-next-line n/no-deprecated-api
const result = url.parse('https://www.datadoghq.com')

sinon.assert.calledOnceWithExactly(parseFinishedChannelCb, {
input: 'https://www.datadoghq.com',
parsed: result,
isURL: false
}, sinon.match.any)
})
})

describe('url.URL', () => {
describe('new URL', () => {
it('should publish with input', () => {
const result = new url.URL('https://www.datadoghq.com')

sinon.assert.calledOnceWithExactly(parseFinishedChannelCb, {
input: 'https://www.datadoghq.com',
base: undefined,
parsed: result,
isURL: true
}, sinon.match.any)
})

it('should publish with base and input', () => {
const result = new url.URL('/path', 'https://www.datadoghq.com')

sinon.assert.calledOnceWithExactly(parseFinishedChannelCb, {
base: 'https://www.datadoghq.com',
input: '/path',
parsed: result,
isURL: true
}, sinon.match.any)
})

;['host', 'origin', 'hostname'].forEach(property => {
it(`should publish on get ${property}`, () => {
const urlObject = new url.URL('/path', 'https://www.datadoghq.com')

const result = urlObject[property]

sinon.assert.calledWithExactly(urlGetterChannelCb, {
urlObject,
result,
property
}, sinon.match.any)
})
})
})
})

if (url.URL.parse) { // added in v22.1.0
describe('url.URL.parse', () => {
it('should publish with input', () => {
const input = 'https://www.datadoghq.com'
const parsed = url.URL.parse(input)

sinon.assert.calledOnceWithExactly(parseFinishedChannelCb, {
input,
parsed,
base: undefined,
isURL: true
}, sinon.match.any)
})

it('should publish with base and input', () => {
const result = new url.URL('/path', 'https://www.datadoghq.com')

sinon.assert.calledOnceWithExactly(parseFinishedChannelCb, {
base: 'https://www.datadoghq.com',
input: '/path',
parsed: result,
isURL: true
}, sinon.match.any)
})
})
}
})
})
41 changes: 41 additions & 0 deletions packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class TaintTrackingPlugin extends SourceIastPlugin {
constructor () {
super()
this._type = 'taint-tracking'
this._taintedURLs = new WeakMap()
}

onConfigure () {
Expand Down Expand Up @@ -88,6 +89,46 @@ class TaintTrackingPlugin extends SourceIastPlugin {
}
)

const urlResultTaintedProperties = ['host', 'origin', 'hostname']
this.addSub(
{ channelName: 'datadog:url:parse:finish' },
({ input, base, parsed, isURL }) => {
const iastContext = getIastContext(storage.getStore())
let ranges

if (base) {
ranges = getRanges(iastContext, base)
} else {
ranges = getRanges(iastContext, input)
}

if (ranges?.length) {
if (isURL) {
this._taintedURLs.set(parsed, ranges[0])
} else {
urlResultTaintedProperties.forEach(param => {
this._taintTrackingHandler(ranges[0].iinfo.type, parsed, param, iastContext)
})
}
}
}
)

this.addSub(
{ channelName: 'datadog:url:getter:finish' },
(context) => {
if (!urlResultTaintedProperties.includes(context.property)) return

const origRange = this._taintedURLs.get(context.urlObject)
if (!origRange) return

const iastContext = getIastContext(storage.getStore())
if (!iastContext) return

context.result =
newTaintedString(iastContext, context.result, origRange.iinfo.parameterName, origRange.iinfo.type)
})

// this is a special case to increment INSTRUMENTED_SOURCE metric for header
this.addInstrumentedSource('http', [HTTP_REQUEST_HEADER_VALUE, HTTP_REQUEST_HEADER_NAME])
}
Expand Down
Loading

0 comments on commit 1ea43eb

Please sign in to comment.