CVE-2014-125098: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: A vulnerability was found in Dart http_server up to 0.9.5 and classified as problematic. Affected by this issue is the function VirtualDirectory of the file lib/src/virtual_directory.dart of the component Directory Listing Handler. The manipulation of the argument request.uri.path leads to cross site scripting. The attack may be launched remotely. Upgrading to version 0.9.6 is able to address this issue. The name of the patch is 27c1cbd8125bb0369e675eb72e48218496e48ffb. It is recommended to upgrade the affected component. The identifier of this vulnerability is VDB-225356.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2023-04-10T04:15Z
Repository: https://github.com/dart-archive/http_server
Language: Other
Files changed: lib/src/virtual_directory.dart, test/virtual_directory_test.dart
Commit date: 2014-04-07 07:03:24 +0000
Commit message: Fix XSS issues in http_server's dir-listing and error-page.

BUG=
R=sgjesse@google.com

Review URL: https://codereview.chromium.org//225813002

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/http_server@34769 260f80e4-7a28-3924-810f-c04153c831b5

Diff: diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart
index 6eef0b4..98dd963 100644
--- a/lib/src/virtual_directory.dart
+++ b/lib/src/virtual_directory.dart
@@ -253,20 +253,21 @@ class VirtualDirectory {
         response.close();
         return;
       }
 
       response.headers.set(HttpHeaders.LAST_MODIFIED, stats.modified);
-      var path = request.uri.path;
+      var path = Uri.decodeComponent(request.uri.path);
+      var encodedPath = new HtmlEscape().convert(path);
       var header =
 '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
-<title>Index of $path</title>
+<title>Index of $encodedPath</title>
 </head>
 <body>
-<h1>Index of $path</h1>
+<h1>Index of $encodedPath</h1>
 <table>
   <tr>
     <td>Name</td>
     <td>Last modified</td>
     <td>Size</td>
@@ -282,20 +283,29 @@ $server
 ''';
 
       response.write(header);
 
       void add(String name, String modified, var size) {
+        try {
         if (size == null) size = "-";
         if (modified == null) modified = "";
-        var p = normalize(join(path, name));
+        var encodedSize = new HtmlEscape().convert(size.toString());
+        var encodedModified = new HtmlEscape().convert(modified);
+        var encodedLink = new HtmlEscape(HtmlEscapeMode.ATTRIBUTE)
+            .convert(Uri.encodeComponent(normalize(join(path, name))));
+        var encodedName = new HtmlEscape().convert(name);
+
         var entry =
 '''  <tr>
-    <td><a href="$p">$name</a></td>
-    <td>$modified</td>
-    <td style="text-align: right">$size</td>
+    <td><a href="$encodedLink">$encodedName</a></td>
+    <td>$encodedModified</td>
+    <td style="text-align: right">$encodedSize</td>
   </tr>''';
         response.write(entry);
+        } catch (e) {
+          print(e);
+        }
       }
 
       if (path != '/') {
         add('../', null, null);
       }
@@ -329,24 +339,26 @@ $server
     if (_errorCallback != null) {
       _errorCallback(request);
       return;
     }
     // Default error page.
-    var path = request.uri.path;
-    var reason = response.reasonPhrase;
+    var path = Uri.decodeComponent(request.uri.path);
+    var encodedPath = new HtmlEscape().convert(path);
+    var encodedReason = new HtmlEscape().convert(response.reasonPhrase);
+    var encodedError = new HtmlEscape().convert(error.toString());
 
     var server = response.headers.value(HttpHeaders.SERVER);
     if (server == null) server = "";
     var page =
 '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
-<title>$reason: $path</title>
+<title>$encodedReason: $encodedPath</title>
 </head>
 <body>
-<h1>Error $error at \'$path\': $reason</h1>
+<h1>Error $encodedError at \'$encodedPath\': $encodedReason</h1>
 $server
 </body>
 </html>''';
     response.write(page);
     response.close();
diff --git a/test/virtual_directory_test.dart b/test/virtual_directory_test.dart
index c04ec34..b58ee31 100644
--- a/test/virtual_directory_test.dart
+++ b/test/virtual_directory_test.dart
@@ -97,11 +97,11 @@ void main() {
         var virDir = new VirtualDirectory(dir.path);
         virDir.allowDirectoryListing = true;
 
         return getAsString(virDir, '/')
           .then((result) {
-            expect(result, contains('Index of /'));
+            expect(result, contains('Index of &#x2F'));
           });
       });
 
       testVirtualDir('files', (dir) {
         var virDir = new VirtualDirectory(dir.path);
@@ -110,11 +110,11 @@ void main() {
         }
         virDir.allowDirectoryListing = true;
 
         return getAsString(virDir, '/')
           .then((result) {
-            expect(result, contains('Index of /'));
+            expect(result, contains('Index of &#x2F'));
           });
       });
 
       testVirtualDir('dirs', (dir) {
         var virDir = new VirtualDirectory(dir.path);
@@ -123,11 +123,45 @@ void main() {
         }
         virDir.allowDirectoryListing = true;
 
         return getAsString(virDir, '/')
           .then((result) {
-            expect(result, contains('Index of /'));
+            expect(result, contains('Index of &#x2F'));
+          });
+      });
+
+      testVirtualDir('encoded-dir', (dir) {
+        var virDir = new VirtualDirectory(dir.path);
+        new Directory('${dir.path}/alert(\'hacked!\');').createSync();
+        virDir.allowDirectoryListing = true;
+
+        return getAsString(virDir, '/alert(\'hacked!\');')
+          .then((result) {
+            expect(result, contains('&#x2F;alert(&#x27;hacked!&#x27;);&#x2F;'));
+          });
+      });
+
+      testVirtualDir('encoded-path', (dir) {
+        var virDir = new VirtualDirectory(dir.path);
+        new Directory('${dir.path}/javascript:alert(document);"').createSync();
+        virDir.allowDirectoryListing = true;
+
+        return getAsString(virDir, '/')
+          .then((result) {
+            expect(result, contains('%2Fjavascript%3Aalert(document)%3B%22'));
+          });
+      });
+
+      testVirtualDir('encoded-special', (dir) {
+        var virDir = new VirtualDirectory(dir.path);
+        new Directory('${dir.path}/<>&"').createSync();
+        virDir.allowDirectoryListing = true;
+
+        return getAsString(virDir, '/')
+          .then((result) {
+            expect(result, contains('&lt;&gt;&amp;&quot;&#x2F;'));
+            expect(result, contains('href="%2F%3C%3E%26%22"'));
           });
       });
 
       if (!Platform.isWindows) {
         testVirtualDir('recursive-link', (dir) {
@@ -135,21 +169,21 @@ void main() {
           var virDir = new VirtualDirectory(dir.path);
           virDir.allowDirectoryListing = true;
 
           return Future.wait([
               getAsString(virDir, '/').then(
-                  (s) => s.contains('recursive/')),
+                  (s) => s.contains('recursive&#x2F;')),
               getAsString(virDir, '/').then(
                   (s) => !s.contains('../')),
               getAsString(virDir, '/').then(
-                  (s) => s.contains('Index of /')),
+                  (s) => s.contains('Index of &#x2F;')),
               getAsString(virDir, '/recursive').then(
-                  (s) => s.contains('recursive/')),
+                  (s) => s.contains('recursive&#x2F;')),
               getAsString(virDir, '/recursive').then(
-                  (s) => s.contains('../')),
+                  (s) => s.contains('..&#x2F;')),
               getAsString(virDir, '/recursive').then(
-                  (s) => s.contains('Index of /recursive'))])
+                  (s) => s.contains('Index of &#x2F;recursive'))])
             .then((result) {
               expect(result, equals([true, true, true, true, true, true]));
             });
         });
       }

Vulnerable code:       var path = request.uri.path;
<title>Index of $path</title>
<h1>Index of $path</h1>
        var p = normalize(join(path, name));
    <td><a href="$p">$name</a></td>
    <td>$modified</td>
    <td style="text-align: right">$size</td>
    var path = request.uri.path;
    var reason = response.reasonPhrase;
<title>$reason: $path</title>
<h1>Error $error at \'$path\': $reason</h1>
            expect(result, contains('Index of /'));
            expect(result, contains('Index of /'));
            expect(result, contains('Index of /'));
                  (s) => s.contains('recursive/')),
                  (s) => s.contains('Index of /')),
                  (s) => s.contains('recursive/')),
                  (s) => s.contains('../')),
                  (s) => s.contains('Index of /recursive'))])
Fixed code:       var path = Uri.decodeComponent(request.uri.path);
      var encodedPath = new HtmlEscape().convert(path);
<title>Index of $encodedPath</title>
<h1>Index of $encodedPath</h1>
        try {
        var encodedSize = new HtmlEscape().convert(size.toString());
        var encodedModified = new HtmlEscape().convert(modified);
        var encodedLink = new HtmlEscape(HtmlEscapeMode.ATTRIBUTE)
            .convert(Uri.encodeComponent(normalize(join(path, name))));
        var encodedName = new HtmlEscape().convert(name);

    <td><a href="$encodedLink">$encodedName</a></td>
    <td>$encodedModified</td>
    <td style="text-align: right">$encodedSize</td>
        } catch (e) {
          print(e);
        }
    var path = Uri.decodeComponent(request.uri.path);
    var encodedPath = new HtmlEscape().convert(path);
    var encodedReason = new HtmlEscape().convert(response.reasonPhrase);
    var encodedError = new HtmlEscape().convert(error.toString());
<title>$encodedReason: $encodedPath</title>
<h1>Error $encodedError at \'$encodedPath\': $encodedReason</h1>
            expect(result, contains('Index of &#x2F'));
            expect(result, contains('Index of &#x2F'));
            expect(result, contains('Index of &#x2F'));
          });
      });

      testVirtualDir('encoded-dir', (dir) {
        var virDir = new VirtualDirectory(dir.path);
        new Directory('${dir.path}/alert(\'hacked!\');').createSync();
        virDir.allowDirectoryListing = true;

        return getAsString(virDir, '/alert(\'hacked!\');')
          .then((result) {
            expect(result, contains('&#x2F;alert(&#x27;hacked!&#x27;);&#x2F;'));
          });
      });

      testVirtualDir('encoded-path', (dir) {
        var virDir = new VirtualDirectory(dir.path);
        new Directory('${dir.path}/javascript:alert(document);"').createSync();
        virDir.allowDirectoryListing = true;

        return getAsString(virDir, '/')
          .then((result) {
            expect(result, contains('%2Fjavascript%3Aalert(document)%3B%22'));
          });
      });

      testVirtualDir('encoded-special', (dir) {
        var virDir = new VirtualDirectory(dir.path);
        new Directory('${dir.path}/<>&"').createSync();
        virDir.allowDirectoryListing = true;

        return getAsString(virDir, '/')
          .then((result) {
            expect(result, contains('&lt;&gt;&amp;&quot;&#x2F;'));
            expect(result, contains('href="%2F%3C%3E%26%22"'));
                  (s) => s.contains('recursive&#x2F;')),
                  (s) => s.contains('Index of &#x2F;')),
                  (s) => s.contains('recursive&#x2F;')),
                  (s) => s.contains('..&#x2F;')),
                  (s) => s.contains('Index of &#x2F;recursive'))])