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

chore: sanitize http.url attribute values #3487

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ For experimental package changes, see the [experimental CHANGELOG](experimental/

### :rocket: (Enhancement)

* feat(opentelemetry-core): sanitize `http.url` attributes to remove usernames and passwords [#3487](https://github.com/open-telemetry/opentelemetry-js/pull/3487) @daniel-white

### :bug: (Bug Fix)

### :books: (Refine Doc)
Expand Down
50 changes: 50 additions & 0 deletions packages/opentelemetry-core/src/common/attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import { diag, SpanAttributeValue, SpanAttributes } from '@opentelemetry/api';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';

export function sanitizeAttributes(attributes: unknown): SpanAttributes {
const out: SpanAttributes = {};
Expand Down Expand Up @@ -94,3 +95,52 @@ function isValidPrimitiveAttributeValue(val: unknown): boolean {

return false;
}

function sanitizeAttributeUsingURL(val: string): string {
let out: string;

try {
const valUrl = new URL(val);

valUrl.username = '';
valUrl.password = '';

out = valUrl.toString();
} catch (err: unknown) {
diag.warn(
`Invalid attribute value set for key: ${SemanticAttributes.HTTP_URL}. Unable to sanitize invalid URL.`
);

out = val;
}

return out;
}

function sanitizeHttpUrlUsingRegExp(val: string): string {
let out = val.replace(/^(https?:\/\/)([^@]+@)?(.*)/, '$1$3');

return out;
}

const sanitizeHttpUrlImpl =
typeof URL === 'function'
? sanitizeAttributeUsingURL
: sanitizeHttpUrlUsingRegExp;

export function sanitizeHttpUrl(val: SpanAttributeValue): SpanAttributeValue {
let out: SpanAttributeValue;

if (typeof val !== 'string') {
diag.warn(
`Invalid attribute value set for key: ${
SemanticAttributes.HTTP_URL
}. Unable to sanitize ${Array.isArray(val) ? 'array' : typeof val} value.`
);

return val;
}

out = sanitizeHttpUrlImpl(val);
return out;
}
88 changes: 88 additions & 0 deletions packages/opentelemetry-core/test/common/attributes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,28 @@
* limitations under the License.
*/

import { diag } from '@opentelemetry/api';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import * as assert from 'assert';
import * as sinon from 'sinon';
import {
isAttributeValue,
sanitizeAttribute,
sanitizeAttributes,
} from '../../src/common/attributes';

describe('attributes', () => {
const warnStub = sinon.fake();

beforeEach(() => {
diag.setLogger({
warn: warnStub,
} as any);

// diag.warn is used when the logger is set
warnStub.resetHistory();
});

describe('#isAttributeValue', () => {
it('should allow primitive values', () => {
assert.ok(isAttributeValue(0));
Expand Down Expand Up @@ -102,5 +117,78 @@ describe('attributes', () => {
assert.ok(Array.isArray(attributes.arr));
assert.strictEqual(attributes.arr[0], 'unmodified');
});

describe('value sanitizers', () => {
describe('http.url', () => {
it('should remove username and password from the url', () => {
const out = sanitizeAttribute(
SemanticAttributes.HTTP_URL,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind converting those test cases into table-based tests to reduce the repetition between tests?

const testData = [
  {
    name: 'should remove username and password from the url',
    input: 'http://user:pass@host:9000/path?query#fragment', 
    expect: 'http://host:9000/path?query#fragment',
  },
  ...
];

for (const item of testData) {
  it(item.name, () => {
    ...
  });
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure that would be possible.

'http://user:pass@host:9000/path?query#fragment'
);

assert.strictEqual(out, 'http://host:9000/path?query#fragment');
assert.ok(warnStub.notCalled, 'should not log warning');
});

it('should return the same url', () => {
const out = sanitizeAttribute(
SemanticAttributes.HTTP_URL,
'http://host:9000/path?query#fragment'
);

assert.strictEqual(out, 'http://host:9000/path?query#fragment');
assert.ok(warnStub.notCalled, 'should not log warning');
});

it('should return the input string when the value is not a valid url', () => {
const out = sanitizeAttribute(
SemanticAttributes.HTTP_URL,
'invalid url'
);

assert.strictEqual(out, 'invalid url');
assert.ok(
warnStub.calledWithExactly(
`Invalid attribute value set for key: ${SemanticAttributes.HTTP_URL}. Unable to sanitize invalid URL.`
)
);
});

it('should return the input when the value is a number', () => {
const out = sanitizeAttribute(SemanticAttributes.HTTP_URL, 27);

assert.strictEqual(out, 27);
assert.ok(
warnStub.calledWithExactly(
`Invalid attribute value set for key: ${SemanticAttributes.HTTP_URL}. Unable to sanitize number value.`
)
);
});

it('should return the input when the value is boolean', () => {
const out = sanitizeAttribute(SemanticAttributes.HTTP_URL, false);

assert.strictEqual(out, false);
assert.ok(
warnStub.calledWithExactly(
`Invalid attribute value set for key: ${SemanticAttributes.HTTP_URL}. Unable to sanitize boolean value.`
)
);
});

it('should return the input when an array is supplied', () => {
const out = sanitizeAttribute(SemanticAttributes.HTTP_URL, [
'http://host/path?query#fragment',
]);

assert.deepStrictEqual(out, ['http://host/path?query#fragment']);
assert.ok(
warnStub.calledWithExactly(
`Invalid attribute value set for key: ${SemanticAttributes.HTTP_URL}. Unable to sanitize array value.`
)
);
});
});
});
});
});
15 changes: 11 additions & 4 deletions packages/opentelemetry-sdk-trace-base/test/common/Span.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import * as assert from 'assert';
import * as sinon from 'sinon';
import { BasicTracerProvider, Span, SpanProcessor } from '../../src';
import { invalidAttributes, validAttributes } from './util';
import {
invalidAttributes,
sanitizedValidAttributes,
validAttributes,
} from './util';

const performanceTimeOrigin: HrTime = [1, 1];

Expand Down Expand Up @@ -264,7 +268,7 @@ describe('Span', () => {
span.setAttribute(k, v as unknown as SpanAttributeValue);
}

assert.deepStrictEqual(span.attributes, validAttributes);
assert.deepStrictEqual(span.attributes, sanitizedValidAttributes);
});

it('should be able to overwrite attributes', () => {
Expand Down Expand Up @@ -712,7 +716,7 @@ describe('Span', () => {
span.setAttributes(validAttributes);
span.setAttributes(invalidAttributes as unknown as SpanAttributes);

assert.deepStrictEqual(span.attributes, validAttributes);
assert.deepStrictEqual(span.attributes, sanitizedValidAttributes);
});
});

Expand Down Expand Up @@ -746,7 +750,10 @@ describe('Span', () => {

assert.strictEqual(span.events.length, 1);
assert.deepStrictEqual(span.events[0].name, 'rev');
assert.deepStrictEqual(span.events[0].attributes, validAttributes);
assert.deepStrictEqual(
span.events[0].attributes,
sanitizedValidAttributes
);
});
});

Expand Down
8 changes: 8 additions & 0 deletions packages/opentelemetry-sdk-trace-base/test/common/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,21 @@
* limitations under the License.
*/

import { SemanticAttributes } from '@opentelemetry/semantic-conventions';

export const validAttributes = {
string: 'string',
number: 0,
bool: true,
'array<string>': ['str1', 'str2'],
'array<number>': [1, 2],
'array<bool>': [true, false],
[SemanticAttributes.HTTP_URL]: 'http://user:pass@host:port/path?query',
};

export const sanitizedValidAttributes = {
...validAttributes,
[SemanticAttributes.HTTP_URL]: 'http://host:port/path?query',
};

export const invalidAttributes = {
Expand Down