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

[ASM] IAST Header injection vulnerability detection. #4981

Merged
merged 78 commits into from
Jan 18, 2024

Conversation

NachoEchevarria
Copy link
Contributor

@NachoEchevarria NachoEchevarria commented Dec 18, 2023

Summary of changes

This PR contains the implementation of the IAST header injection vulnerability detection.

The header injection vulnerability is launched when a header added to the response contains values that are tainted (coming from untrusted sources).

The full description of the vulnerability can be found here: https://datadoghq.atlassian.net/wiki/spaces/APS/pages/3295019091/Header+Injection

Reason for change

It's in the IAST roadmap

Implementation details

Test coverage

Other details

return (
taintedValue.Ranges.Length == 1 &&
taintedValue.Ranges[0].Source is not null &&
taintedValue.Ranges[0].Source!.OriginByte == (byte)SourceTypeName.RequestHeaderValue &&
Copy link
Contributor

Choose a reason for hiding this comment

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

I think OriginByte is not needed (it is not serialized) and makes code reading more difficult. It should be a SourceType directly

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you mean comparing the string value instead of the byte? This would be more expensive in terms of performance than just comparing a single byte. The byte value should always be present even if we don't serialize it.

Copy link
Contributor

Choose a reason for hiding this comment

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

I mean using the enum instead of a raw byte

NachoEchevarria and others added 3 commits January 8, 2024 15:00
Co-authored-by: Daniel Romano <108014683+daniel-romano-DD@users.noreply.github.com>
Copy link
Contributor

@daniel-romano-DD daniel-romano-DD left a comment

Choose a reason for hiding this comment

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

LGTM with minor NITS

Copy link
Member

@andrewlock andrewlock left a comment

Choose a reason for hiding this comment

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

LGTM in general, just a few question!

AnalyzeHeaderInjectionVulnerability(responseHeaders, integrationId);
}

#if NETFRAMEWORK
Copy link
Member

Choose a reason for hiding this comment

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

I agree, I suggest extracting this as two different methods, and then extracting the body of the method to a shared helper function

tracer/src/Datadog.Trace/Iast/ReturnedHeadersAnalyzer.cs Outdated Show resolved Hide resolved
continue;
}

string headerValue = responseHeaders[headerKey];
Copy link
Member

Choose a reason for hiding this comment

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

In .NET Core, the return value is StringValues. By casting to a string, you will throw an exception if there are actually multiple header values.

A better option is to handle the possibility of there being more than one value set, and handle it appropriately. Luckily, I wrote a blog post about this 3 days ago 😂 https://andrewlock.net/a-brief-look-at-stringvalues/

Also, just FYI, if the same header is set multiple times in .NET FX NameValueCollection will return them here using string.Join(",", values). That may be ok for you here, but I don't know if you have anything in the spec about how to handle this situation (i.e. the header is set more than once)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have updated the code to handle multiple values in one header for both netcore and net framework. In Net framework, I have used GetValues to get all the values and avoid the join. I have also added integration tests that cover this case. Thank you!

Copy link
Member

Choose a reason for hiding this comment

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

In Net framework, I have used GetValues to get all the values and avoid the join.

So there's definitely a question of whether we should do that tbh... By using GetValues() we're always allocating an array for every accessed header. I wonder if it's actually a problem or not if we use GetValue() and join the values with a ','? Happy with whatever you decide, just wanted to flag it 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The problem with the "," separated values is that we loose the tainting since the join is done in libraries that we don't instrument, so we would not be able to know if the value is tainted.

tracer/src/Datadog.Trace/Iast/ReturnedHeadersAnalyzer.cs Outdated Show resolved Hide resolved
tracer/src/Datadog.Trace/Iast/ReturnedHeadersAnalyzer.cs Outdated Show resolved Hide resolved
if (separatorStart > 0)
{
var separatorEnd = separatorStart + IastModule.HeaderInjectionEvidenceSeparator.Length;
var valuePart = evidence.Substring(separatorEnd);
Copy link
Member

Choose a reason for hiding this comment

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

nit: you may able to avoid the substring here in .NET Core? I haven't checked if Regex works on spans, but might be worth a look

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, the overload public bool IsMatch(string input, int startat) can be used also in .Net frameowrk. Thanks!

}
catch (Exception ex)
{
Log.Error(ex, $"Error in {nameof(VulnerabilityBatch)}.{nameof(ToJson)}");
Copy link
Member

Choose a reason for hiding this comment

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

Just wondering when this might throw? We need to be careful using Log.Error if it could fail in "normal" execution, because it can flood the logs (and also flood our telemetry)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There should be no exceptions in normal execution inside this code. If we have an exception here, we should take a look because there will probably be a bug or unexpected situation.

"value": ": "
},
{
"value": ":bearer secret",
Copy link
Member

Choose a reason for hiding this comment

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

I assumed this should be redacted if it's sensitive, no? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this test, redaction is disabled. The enabled version is in Iast.HeaderInjection.AspNetCore2.Vuln.SensitiveValue.RedactionEnabled.verified.txt

},
{
"redacted": true,
"pattern": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrs",
Copy link
Member

Choose a reason for hiding this comment

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

Where is this value coming from, I can't seem to find it? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a redaction pattern that is replacing the real value

Copy link
Member

@andrewlock andrewlock left a comment

Choose a reason for hiding this comment

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

LGTM, thanks! Just one comment about adding a try-catch now we have potential regex timeouts :)

Comment on lines 41 to 45
if (_keyPattern.IsMatch(evidence.Substring(0, separatorStart)) ||
_valuePattern.IsMatch(evidence, separatorEnd))
{
return [new Range(separatorEnd, evidence.Length - separatorEnd)];
}
Copy link
Member

Choose a reason for hiding this comment

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

I think you need to add a try-catch here in case the regexes timeout?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The exceptions here would be catch in VulnerabilityBatch.ToJson(), that calls this method. Still, thinking about it, it's worth to add a catch for the RegexMatchTimeoutException here since I don't think that this kind of exception should launch an error but a warning. If any other exception happens, an error should be launched. Done! Thanks!

@NachoEchevarria
Copy link
Contributor Author

Thank very much you for your feedback!

@NachoEchevarria NachoEchevarria merged commit 40a49fb into master Jan 18, 2024
52 of 55 checks passed
@NachoEchevarria NachoEchevarria deleted the nacho/headerInjection branch January 18, 2024 09:27
@github-actions github-actions bot added this to the vNext milestone Jan 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants