Skip to content

Commit 0f41c5b

Browse files
Add S5734 Require nosniff header in web.config
1 parent 766bf08 commit 0f41c5b

File tree

14 files changed

+310
-5
lines changed

14 files changed

+310
-5
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"project:asp.net-web-application/web.config": [
3+
1
4+
]
5+
}

sonar-xml-plugin/src/main/java/org/sonar/plugins/xml/checks/CheckList.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.sonar.plugins.xml.checks.security.web.BasicAuthenticationCheck;
4141
import org.sonar.plugins.xml.checks.security.web.CrossOriginResourceSharingCheck;
4242
import org.sonar.plugins.xml.checks.security.web.HttpOnlyOnCookiesCheck;
43+
import org.sonar.plugins.xml.checks.security.web.MimeNosniffCheck;
4344
import org.sonar.plugins.xml.checks.security.web.ValidationFiltersCheck;
4445
import org.sonar.plugins.xml.checks.spring.DefaultMessageListenerContainerCheck;
4546
import org.sonar.plugins.xml.checks.spring.SingleConnectionFactoryCheck;
@@ -89,7 +90,8 @@ public static List<Class<?>> getCheckClasses() {
8990
FixmeCommentCheck.class,
9091
ValidationFiltersCheck.class,
9192
DisallowedDependenciesCheck.class,
92-
CommentedOutCodeCheck.class
93+
CommentedOutCodeCheck.class,
94+
MimeNosniffCheck.class
9395
);
9496
}
9597

sonar-xml-plugin/src/main/java/org/sonar/plugins/xml/checks/security/web/BaseWebCheck.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,28 @@ protected void scanWebConfig(XmlFile file) {
4848
private static boolean isWebXmlFile(XmlFile file) {
4949
return "web.xml".equalsIgnoreCase(file.getInputFile().filename());
5050
}
51+
52+
/**
53+
* Builds an XPath expression that matches the deepest existing node.
54+
*
55+
* For example, for segments "a", "b", "c", the resulting expression is:
56+
* <pre>
57+
* /a/b/c | /a/b[not(c)] | /a[not(b)]
58+
* </pre>
59+
*/
60+
protected String getDeepestExistingNode(String ... segments) {
61+
StringBuilder expression = new StringBuilder();
62+
for (int len = segments.length; len > 0; len--) {
63+
for (int i = 0; i < len; i++) {
64+
expression.append("/").append(segments[i]);
65+
}
66+
if (len < segments.length) {
67+
expression.append("[not(").append(segments[len]).append(")]");
68+
}
69+
if (len > 1) {
70+
expression.append("|");
71+
}
72+
}
73+
return expression.toString();
74+
}
5175
}

sonar-xml-plugin/src/main/java/org/sonar/plugins/xml/checks/security/web/HttpOnlyOnCookiesCheck.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,7 @@ public class HttpOnlyOnCookiesCheck extends BaseWebCheck {
4646

4747
/// Closest existing node if the global `<httpCookies>` is missing or misconfigured.
4848
private final XPathExpression reportNodeExpression = XPathBuilder
49-
.forExpression(
50-
"/configuration/system.web/httpCookies | " +
51-
"/configuration/system.web[not(httpCookies)] | " +
52-
"/configuration[not(system.web)]")
49+
.forExpression(getDeepestExistingNode("configuration", "system.web", "httpCookies"))
5350
.build();
5451

5552
@Override
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* SonarQube XML Plugin
3+
* Copyright (C) 2010-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.plugins.xml.checks.security.web;
18+
19+
import org.sonar.check.Rule;
20+
import org.sonarsource.analyzer.commons.xml.XPathBuilder;
21+
import org.sonarsource.analyzer.commons.xml.XmlFile;
22+
import org.w3c.dom.Document;
23+
import org.w3c.dom.Element;
24+
import org.w3c.dom.NodeList;
25+
26+
import javax.xml.xpath.XPathExpression;
27+
import java.util.Collections;
28+
29+
/**
30+
* Ensure that the X-Content-Type-Options header is set to "nosniff" to prevent MIME type sniffing.
31+
* The check applies to .NET web.config files.
32+
*/
33+
@Rule(key = "S5734")
34+
public class MimeNosniffCheck extends BaseWebCheck {
35+
private final XPathExpression httpCookiesExpression = XPathBuilder
36+
.forExpression(
37+
"/configuration"
38+
+ "/system.webServer"
39+
+ "/httpProtocol"
40+
+ "/customHeaders"
41+
+ "/add[@name=\"X-Content-Type-Options\" and @value=\"nosniff\"]")
42+
.build();
43+
44+
/** Attach the issue to the closest existing node. */
45+
private final XPathExpression reportNodeExpression = XPathBuilder
46+
.forExpression(getDeepestExistingNode("configuration", "system.webServer", "httpProtocol", "customHeaders"))
47+
.build();
48+
49+
@Override
50+
protected void scanWebConfig(XmlFile file) {
51+
Document document = file.getDocument();
52+
NodeList expectedNodes = evaluate(httpCookiesExpression, document);
53+
54+
// null is returned on internal errors, and we don't want to raise a false positive in that case.
55+
if (expectedNodes != null && expectedNodes.getLength() == 0) {
56+
evaluateAsList(reportNodeExpression, document)
57+
.stream()
58+
.findFirst()
59+
.ifPresent(target ->
60+
reportIssue(
61+
XmlFile.nameLocation((Element) target),
62+
"Global <httpCookies> tag is missing or its 'httpOnlyCookies' attribute is not set to true.",
63+
Collections.emptyList()));
64+
}
65+
}
66+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<p><a href="https://blog.mozilla.org/security/2016/08/26/mitigating-mime-confusion-attacks-in-firefox/">MIME confusion</a> attacks occur when an
2+
attacker successfully tricks a web-browser to interpret a resource as a different type than the one expected. To correctly interpret a resource
3+
(script, image, stylesheet …​) web browsers look for the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type">Content-Type
4+
header</a> defined in the HTTP response received from the server, but often this header is not set or is set with an incorrect value. To avoid
5+
content-type mismatch and to provide the best user experience, web browsers try to deduce the right content-type, generally by inspecting the content
6+
of the resources (the first bytes). This "guess mechanism" is called <a href="https://en.wikipedia.org/wiki/Content_sniffing">MIME type
7+
sniffing</a>.</p>
8+
<p>Attackers can take advantage of this feature when a website ("example.com" here) allows to upload arbitrary files. In that case, an attacker can
9+
upload a malicious image <em>fakeimage.png</em> (containing malicious JavaScript code or <a
10+
href="https://docs.microsoft.com/fr-fr/archive/blogs/ieinternals/script-polyglots">a polyglot content</a> file) such as:</p>
11+
<pre>
12+
&lt;script&gt;alert(document.cookie)&lt;/script&gt;
13+
</pre>
14+
<p>When the victim will visit the website showing the uploaded image, the malicious script embedded into the image will be executed by web browsers
15+
performing MIME type sniffing.</p>
16+
<h2>Ask Yourself Whether</h2>
17+
<ul>
18+
<li> <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type">Content-Type</a> header is not systematically set for all
19+
resources. </li>
20+
<li> Content of resources can be controlled by users. </li>
21+
</ul>
22+
<p>There is a risk if you answered yes to any of those questions.</p>
23+
<h2>Recommended Secure Coding Practices</h2>
24+
<p>Implement <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options">X-Content-Type-Options</a> header with
25+
<em>nosniff</em> value (the only existing value for this header) which is supported by all modern browsers and will prevent browsers from performing
26+
MIME type sniffing, so that in case of Content-Type header mismatch, the resource is not interpreted. For example within a &lt;script&gt; object
27+
context, JavaScript MIME types are expected (like <em>application/javascript</em>) in the Content-Type header.</p>
28+
<h2>Sensitive Code Example</h2>
29+
<p>The following ASP.NET website configuration is sensitive, because it does not set the <code>X-Content-Type-Options</code> HTTP response header to
30+
<code>nosniff</code>:</p>
31+
<pre>
32+
&lt;?xml version="1.0" encoding="utf-8"?&gt;
33+
&lt;configuration&gt; &lt;!-- Sensitive --&gt;
34+
&lt;system.webServer&gt;
35+
&lt;!-- ... --&gt;
36+
&lt;/system.webServer&gt;
37+
&lt;/configuration&gt;
38+
</pre>
39+
<h2>Compliant Solution</h2>
40+
<p>To mitigate this finding, set <code>nosniff</code> globally for all pages in the application in the <code>web.config</code> file. While it is also
41+
possible to configure the header individually in the application code, or per <code>&lt;location&gt;</code> element, setting it globally is less
42+
error-prone and therefore recommended.</p>
43+
<pre>
44+
&lt;?xml version="1.0" encoding="utf-8"?&gt;
45+
&lt;configuration&gt;
46+
&lt;system.webServer&gt;
47+
&lt;httpProtocol&gt;
48+
&lt;customHeaders&gt;
49+
&lt;add name="X-Content-Type-Options" value="nosniff"/&gt;
50+
&lt;/customHeaders&gt;
51+
&lt;/httpProtocol&gt;
52+
&lt;!-- ... --&gt;
53+
&lt;/system.webServer&gt;
54+
&lt;/configuration&gt;
55+
</pre>
56+
<h2>See</h2>
57+
<ul>
58+
<li> OWASP - <a href="https://owasp.org/Top10/A05_2021-Security_Misconfiguration/">Top 10 2021 Category A5 - Security Misconfiguration</a> </li>
59+
<li> OWASP - <a href="https://owasp.org/www-project-top-ten/2017/A6_2017-Security_Misconfiguration">Top 10 2017 Category A6 - Security
60+
Misconfiguration</a> </li>
61+
<li> <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options">developer.mozilla.org</a> - X-Content-Type-Options
62+
</li>
63+
<li> <a href="https://blog.mozilla.org/security/2016/08/26/mitigating-mime-confusion-attacks-in-firefox/">blog.mozilla.org</a> - Mitigating MIME
64+
Confusion Attacks in Firefox </li>
65+
</ul>
66+
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"title": "Allowing browsers to sniff MIME types is security-sensitive",
3+
"type": "SECURITY_HOTSPOT",
4+
"code": {
5+
"impacts": {
6+
"SECURITY": "LOW"
7+
},
8+
"attribute": "COMPLETE"
9+
},
10+
"status": "ready",
11+
"remediation": {
12+
"func": "Constant\/Issue",
13+
"constantCost": "5min"
14+
},
15+
"tags": [],
16+
"defaultSeverity": "Minor",
17+
"ruleSpecification": "RSPEC-5734",
18+
"sqKey": "S5734",
19+
"scope": "Main",
20+
"securityStandards": {
21+
"OWASP": [
22+
"A6"
23+
],
24+
"OWASP Top 10 2021": [
25+
"A5"
26+
]
27+
}
28+
}

sonar-xml-plugin/src/main/resources/org/sonar/l10n/xml/rules/xml/Sonar_way_profile.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"S5322",
2121
"S5332",
2222
"S5604",
23+
"S5734",
2324
"S6358",
2425
"S6359",
2526
"S6361",
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* SonarQube XML Plugin
3+
* Copyright (C) 2010-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.plugins.xml.checks.security.web;
18+
19+
import org.junit.jupiter.api.Test;
20+
import static org.assertj.core.api.Assertions.assertThat;
21+
22+
class BaseWebCheckTest {
23+
@Test
24+
void testGetDeepestExistingNode() {
25+
BaseWebCheck baseWebCheck = new BaseWebCheck();
26+
27+
assertThat(baseWebCheck.getDeepestExistingNode("a"))
28+
.isEqualTo("/a");
29+
30+
assertThat(baseWebCheck.getDeepestExistingNode("a", "b"))
31+
.isEqualTo("/a/b|/a[not(b)]");
32+
33+
assertThat(baseWebCheck.getDeepestExistingNode("a", "b", "c"))
34+
.isEqualTo("/a/b/c|/a/b[not(c)]|/a[not(b)]");
35+
}
36+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* SonarQube XML Plugin
3+
* Copyright (C) 2010-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.plugins.xml.checks.security.web;
18+
19+
import java.nio.file.Paths;
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.params.ParameterizedTest;
22+
import org.junit.jupiter.params.provider.ValueSource;
23+
import org.sonarsource.analyzer.commons.xml.checks.SonarXmlCheckVerifier;
24+
25+
class MimeNosniffCheckTest {
26+
27+
@Test
28+
void compliant() {
29+
String path = Paths.get("webconfig-compliant", "web.config").toString();
30+
SonarXmlCheckVerifier.verifyNoIssue(path, new MimeNosniffCheck());
31+
}
32+
33+
@ParameterizedTest
34+
@ValueSource(strings = {
35+
"webconfig-no-custom-headers",
36+
"webconfig-missing-nosniff",
37+
"webconfig-other-value"
38+
})
39+
void noncompliant(String dirName) {
40+
String path = Paths.get(dirName, "web.config").toString();
41+
SonarXmlCheckVerifier.verifyIssues(path, new MimeNosniffCheck());
42+
}
43+
}

0 commit comments

Comments
 (0)