diff --git a/manifests/dotnet.yml b/manifests/dotnet.yml index 5410d180d92..4a53373539a 100644 --- a/manifests/dotnet.yml +++ b/manifests/dotnet.yml @@ -36,6 +36,10 @@ tests/: Test_HardcodedSecretsExtended: missing_feature test_header_injection.py: TestHeaderInjection: v2.46.0 + TestHeaderInjectionExclusionAccessControlAllow: missing_feature + TestHeaderInjectionExclusionContentEncoding: missing_feature + TestHeaderInjectionExclusionPragma: missing_feature + TestHeaderInjectionExclusionTransferEncoding: missing_feature test_hsts_missing_header.py: Test_HstsMissingHeader: v2.44.0 test_insecure_auth_protocol.py: diff --git a/manifests/golang.yml b/manifests/golang.yml index 29728ef4ad1..d4fdc68969f 100644 --- a/manifests/golang.yml +++ b/manifests/golang.yml @@ -45,6 +45,10 @@ tests/: Test_HardcodedSecretsExtended: missing_feature test_header_injection.py: TestHeaderInjection: missing_feature + TestHeaderInjectionExclusionAccessControlAllow: missing_feature + TestHeaderInjectionExclusionContentEncoding: missing_feature + TestHeaderInjectionExclusionPragma: missing_feature + TestHeaderInjectionExclusionTransferEncoding: missing_feature test_hsts_missing_header.py: Test_HstsMissingHeader: missing_feature test_insecure_auth_protocol.py: diff --git a/manifests/java.yml b/manifests/java.yml index 92ab026eb3f..4bcedc21889 100644 --- a/manifests/java.yml +++ b/manifests/java.yml @@ -89,6 +89,10 @@ tests/: spring-boot-undertow: v1.27.0 spring-boot-wildfly: v1.27.0 uds-spring-boot: v1.27.0 + TestHeaderInjectionExclusionAccessControlAllow: missing_feature + TestHeaderInjectionExclusionContentEncoding: missing_feature + TestHeaderInjectionExclusionPragma: missing_feature + TestHeaderInjectionExclusionTransferEncoding: missing_feature test_hsts_missing_header.py: Test_HstsMissingHeader: '*': v1.20.0 diff --git a/manifests/nodejs.yml b/manifests/nodejs.yml index 13dcf43e213..8d14c98b1cb 100644 --- a/manifests/nodejs.yml +++ b/manifests/nodejs.yml @@ -38,6 +38,7 @@ refs: - &ref_5_23_0 '>=5.23.0 || ^4.47.0' - &ref_5_24_0 '>=5.24.0 || ^4.48.0' - &ref_5_25_0 '>=5.25.0 || ^4.49.0' + - &ref_5_26_0 '>=5.26.0 || ^4.50.0' tests/: apm_tracing_e2e/: @@ -96,6 +97,18 @@ tests/: TestHeaderInjection: '*': *ref_4_21_0 nextjs: missing_feature + TestHeaderInjectionExclusionAccessControlAllow: + '*': *ref_5_26_0 + nextjs: missing_feature + TestHeaderInjectionExclusionContentEncoding: + '*': *ref_5_26_0 + nextjs: missing_feature + TestHeaderInjectionExclusionPragma: + '*': *ref_5_26_0 + nextjs: missing_feature + TestHeaderInjectionExclusionTransferEncoding: + '*': *ref_5_26_0 + nextjs: missing_feature test_hsts_missing_header.py: Test_HstsMissingHeader: '*': *ref_4_8_0 diff --git a/manifests/php.yml b/manifests/php.yml index 4d8e683ebbb..1f02095ab8d 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -36,6 +36,10 @@ tests/: Test_HardcodedSecretsExtended: missing_feature test_header_injection.py: TestHeaderInjection: missing_feature + TestHeaderInjectionExclusionAccessControlAllow: missing_feature + TestHeaderInjectionExclusionContentEncoding: missing_feature + TestHeaderInjectionExclusionPragma: missing_feature + TestHeaderInjectionExclusionTransferEncoding: missing_feature test_hsts_missing_header.py: Test_HstsMissingHeader: missing_feature test_insecure_auth_protocol.py: diff --git a/manifests/python.yml b/manifests/python.yml index b6ec3496f04..5b87a207ba9 100644 --- a/manifests/python.yml +++ b/manifests/python.yml @@ -74,6 +74,10 @@ tests/: TestHeaderInjection: '*': v2.10.0 fastapi: missing_feature + TestHeaderInjectionExclusionAccessControlAllow: missing_feature + TestHeaderInjectionExclusionContentEncoding: missing_feature + TestHeaderInjectionExclusionPragma: missing_feature + TestHeaderInjectionExclusionTransferEncoding: missing_feature test_hsts_missing_header.py: Test_HstsMissingHeader: missing_feature test_insecure_auth_protocol.py: diff --git a/manifests/ruby.yml b/manifests/ruby.yml index c41182a6829..d374ab23aa0 100644 --- a/manifests/ruby.yml +++ b/manifests/ruby.yml @@ -38,6 +38,10 @@ tests/: Test_HardcodedSecretsExtended: missing_feature test_header_injection.py: TestHeaderInjection: missing_feature + TestHeaderInjectionExclusionAccessControlAllow: missing_feature + TestHeaderInjectionExclusionContentEncoding: missing_feature + TestHeaderInjectionExclusionPragma: missing_feature + TestHeaderInjectionExclusionTransferEncoding: missing_feature test_hsts_missing_header.py: Test_HstsMissingHeader: missing_feature test_insecure_auth_protocol.py: diff --git a/tests/appsec/iast/sink/test_header_injection.py b/tests/appsec/iast/sink/test_header_injection.py index 242f7587616..1dff965071d 100644 --- a/tests/appsec/iast/sink/test_header_injection.py +++ b/tests/appsec/iast/sink/test_header_injection.py @@ -2,8 +2,48 @@ # This product includes software developed at Datadog (https://www.datadoghq.com/). # Copyright 2021 Datadog, Inc. -from utils import context, features, missing_feature -from ..utils import BaseSinkTest +from utils import context, features, missing_feature, weblog +from ..utils import BaseSinkTest, assert_iast_vulnerability + + +class _BaseTestHeaderInjectionReflectedExclusion: + origin_header: None + reflected_header: None + headers: None + + exclusion_request: None + no_exclusion_request: None + + def setup_no_exclusion(self): + assert self.origin_header is not None, f"Please set {self}.origin_header" + assert isinstance(self.origin_header, str), f"Please set {self}.origin_header" + assert self.reflected_header is not None, f"Please set {self}.reflected_header" + assert isinstance(self.reflected_header, str), f"Please set {self}.reflected_header" + + self.no_exclusion_request = weblog.get( + path="/iast/header_injection/reflected/no-exclusion", + params={"origin": self.origin_header, "reflected": self.reflected_header}, + ) + + def test_no_exclusion(self): + assert_iast_vulnerability( + request=self.no_exclusion_request, vulnerability_count=1, vulnerability_type="HEADER_INJECTION", + ) + + def setup_exclusion(self): + assert self.origin_header is not None, f"Please set {self}.origin_header" + assert isinstance(self.origin_header, str), f"Please set {self}.origin_header" + assert self.reflected_header is not None, f"Please set {self}.reflected_header" + assert isinstance(self.reflected_header, str), f"Please set {self}.reflected_header" + + self.exclusion_request = weblog.get( + path="/iast/header_injection/reflected/exclusion", + params={"origin": self.origin_header, "reflected": self.reflected_header}, + headers=self.headers, + ) + + def test_exclusion(self): + BaseSinkTest.assert_no_iast_event(self.exclusion_request) @features.iast_sink_header_injection @@ -25,3 +65,39 @@ def test_telemetry_metric_instrumented_sink(self): @missing_feature(context.library < "java@1.22.0", reason="Metrics not implemented") def test_telemetry_metric_executed_sink(self): super().test_telemetry_metric_executed_sink() + + +@features.iast_sink_header_injection +class TestHeaderInjectionExclusionAccessControlAllow(_BaseTestHeaderInjectionReflectedExclusion): + """Verify Header injection Access-Control-Allow-* reflexion exclusion""" + + origin_header = "x-custom-header" + reflected_header = "access-control-allow-origin" + headers = {"x-custom-header": "allowed-origin"} + + +@features.iast_sink_header_injection +class TestHeaderInjectionExclusionContentEncoding(_BaseTestHeaderInjectionReflectedExclusion): + """Verify Header injection Content-Encoding reflexion exclusion""" + + origin_header = "accept-encoding" + reflected_header = "content-encoding" + headers = {"accept-encoding": "foo, bar"} + + +@features.iast_sink_header_injection +class TestHeaderInjectionExclusionPragma(_BaseTestHeaderInjectionReflectedExclusion): + """Verify Header injection Pragma reflexion exclusion""" + + origin_header = "cache-control" + reflected_header = "pragma" + headers = {"cache-control": "cacheControlValue"} + + +@features.iast_sink_header_injection +class TestHeaderInjectionExclusionTransferEncoding(_BaseTestHeaderInjectionReflectedExclusion): + """Verify Header injection Transfer-Encoding reflexion exclusion""" + + origin_header = "accept-encoding" + reflected_header = "transfer-encoding" + headers = {"accept-encoding": "foo, bar"} diff --git a/utils/build/docker/nodejs/express4-typescript/iast.ts b/utils/build/docker/nodejs/express4-typescript/iast.ts index 754236c366e..505e3d4d7c2 100644 --- a/utils/build/docker/nodejs/express4-typescript/iast.ts +++ b/utils/build/docker/nodejs/express4-typescript/iast.ts @@ -317,6 +317,40 @@ function initSinkRoutes (app: Express): void { res.send(`OK:${token}`) }) + app.get('/iast/header_injection/reflected/exclusion', ({ headers, query }: Request, res: Response): void => { + const reflectedHeaderName: string = `${query.reflected}` + const originHeaderName: string = `${query.origin}` + res.setHeader(reflectedHeaderName, `${headers[originHeaderName]}`) + res.send('OK') + }) + + app.get('/iast/header_injection/reflected/no-exclusion', ({ query }: Request, res: Response): void => { + // There is a reason for this: to avoid vulnerabilities deduplication, + // which caused the non-exclusion test to fail for all tests after the first one, + // since they are all in the same location (the hash is calculated based on the location). + + const reflectedHeaderName: string = `${query.reflected}` + const originHeaderName: string = `${query.origin}` + switch (reflectedHeaderName) { + case 'pragma': + res.setHeader(reflectedHeaderName, originHeaderName) + break + case 'transfer-encoding': + res.setHeader(reflectedHeaderName, originHeaderName) + break + case 'content-encoding': + res.setHeader(reflectedHeaderName, originHeaderName) + break + case 'access-control-allow-origin': + res.setHeader(reflectedHeaderName, originHeaderName) + break + default: + res.setHeader(reflectedHeaderName, originHeaderName) + break + } + res.send('OK') + }) + app.post('/iast/header_injection/test_insecure', (req: Request, res: Response): void => { res.setHeader('testheader', req.body.test) res.send('OK') diff --git a/utils/build/docker/nodejs/express4/iast/index.js b/utils/build/docker/nodejs/express4/iast/index.js index 3dae1cdaa51..3e50e08d974 100644 --- a/utils/build/docker/nodejs/express4/iast/index.js +++ b/utils/build/docker/nodejs/express4/iast/index.js @@ -302,6 +302,36 @@ function initRoutes (app, tracer) { res.send(`OK:${token}`) }) + app.get('/iast/header_injection/reflected/exclusion', (req, res) => { + res.setHeader(req.query.reflected, req.headers[req.query.origin]) + res.send('OK') + }) + + app.get('/iast/header_injection/reflected/no-exclusion', (req, res) => { + // There is a reason for this: to avoid vulnerabilities deduplication, + // which caused the non-exclusion test to fail for all tests after the first one, + // since they are all in the same location (the hash is calculated based on the location). + + switch (req.query.reflected) { + case 'pragma': + res.setHeader(req.query.reflected, req.query.origin) + break + case 'transfer-encoding': + res.setHeader(req.query.reflected, req.query.origin) + break + case 'content-encoding': + res.setHeader(req.query.reflected, req.query.origin) + break + case 'access-control-allow-origin': + res.setHeader(req.query.reflected, req.query.origin) + break + default: + res.setHeader(req.query.reflected, req.query.origin) + break + } + res.send('OK') + }) + app.post('/iast/header_injection/test_insecure', (req, res) => { res.setHeader('testheader', req.body.test) res.send('OK')