CVE-2022-21650

Description

The Convos is an open source multi-user chat that runs in a web browser. You can’t use SVG extension in Convos chat window, but you can upload .html extension. This causes Stored XSS. Also, after uploading a file, it does not log in, and XSS occurs even if you connect.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
sub upload {
my $self = shift;

# TODO: Move this to Mojolicious::Plugin::OpenAPI
# Handle "Maximum message size exceeded"
my $error = $self->req->error;
$self->reply->errors([[$error->{message}, '/file']], 400) if $error;

return unless $self->openapi->valid_input;
return $self->reply->errors([], 401) unless my $user = $self->backend->user;

my $upload = $self->req->upload('file');
my $err;
return $self->reply->errors([[$err, '/file']], 400)
if $err = !$upload ? 'No upload.' : !$upload->filename ? 'Unknown filename.' : '';

return $self->_validate_upload_p($upload)->then(sub {
my %meta = (filename => $upload->filename);
$meta{id} = $self->param('id') if defined $self->param('id');
$meta{write_only} = $self->param('write_only') if defined $self->param('write_only');

my $asset = $upload->asset;
$asset = $asset->to_file unless $asset->is_file;

# The iPhone uploads every photo as "image.jpg"
if ($meta{filename} =~ /^image.jpe?g$/i) {
my $n = time % 10000;
$meta{filename} = "IMG_$n.jpg";
}

return $self->_file(%meta, asset => $asset, user => $self->backend->user)->save_p;
})->then(sub {
return $self->render(openapi => {files => [shift]});
});
}

sub _file { shift->app->config('file_class')->new(@_) }

sub _validate_upload_p {
my ($self, $upload) = @_;
state $RULES = {svg => qr{<script\b|\bjavascript\W|\son\w+=}i};

return Mojo::Promise->reject('Cannot upload file without extension.')
unless my $ext = $upload->filename =~ m!\.(\w+)$!i && lc $1;
return Mojo::Promise->resolve unless my $re = $RULES->{$ext};

return Mojo::IOLoop->subprocess->run_p(sub {
die {message => "Uploaded $ext looks like a xss attack.", status => 400}
if $upload->asset->slurp =~ $re;
});
}
# https://github.com/convos-chat/convos/blob/main/lib/Convos/Controller/Files.pm#L62L112

The sub upload part is the code to upload the file. If you look inside sub upload, you can see that the file extension is being checked using the $self->_validate_upload_p() function.

When I saw the _validate_upload_p() function, I could confirm that only the SVG extension was checked. This means you can upload HTML files.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
    ->header_is('Content-Type' => 'image/jpeg')->header_exists_not('Content-Disposition');
};

- subtest 'binary' => sub {
+ subtest 'attachment' => sub {
+ note 'binary';
$t->post_ok('/api/files', form => {file => {file => 't/data/binary.bin'}})->status_is(200);
my $fid = $t->tx->res->json('/files/0/id');
my $url = $t->tx->res->json('/files/0/url');
my $name = $t->tx->res->json('/files/0/filename');
$t->get_ok("/file/1/$fid")->header_is('Cache-Control', 'max-age=86400')
->header_is('Content-Disposition', 'attachment; filename="binary.bin"')
->header_is('Content-Type' => 'application/octet-stream');
+
+ note 'html';
+ $t->post_ok('/api/files', form => {file => {file => 't/data/markup.html'}})->status_is(200);
+ $fid = $t->tx->res->json('/files/0/id');
+ $t->get_ok("/file/1/$fid.html")
+ ->header_is('Content-Disposition', 'attachment; filename="markup.html"')
+ ->header_is('Content-Type' => 'text/html;charset=UTF-8')->content_like(qr{^<!DOCTYPE html>});
+ $t->get_ok("/file/1/$fid")->header_is('Content-Disposition', undef)
+ ->header_is('Content-Type' => 'text/html;charset=UTF-8')->text_is('h1', 'markup.html');
+ like $t->tx->res->dom->at('div.le-paste'), qr{&lt;!DOCTYPE}, 'embedded and escaped html';
+
+ note 'xhtml';
+ $t->post_ok('/api/files', form => {file => {file => 't/data/markup.xhtml'}})->status_is(200);
+ $fid = $t->tx->res->json('/files/0/id');
+ $t->get_ok("/file/1/$fid.xhtml")
+ ->header_is('Content-Disposition', 'attachment; filename="markup.xhtml"')
+ ->header_is('Content-Type' => 'application/xhtml+xml')
+ ->content_like(qr{^<!DOCTYPE html PUBLIC});
+ $t->get_ok("/file/1/$fid")->header_is('Content-Disposition', undef)
+ ->header_is('Content-Type' => 'text/html;charset=UTF-8')->text_is('h1', 'markup.xhtml');
+ like $t->tx->res->dom->at('div.le-paste'), qr{&lt;!DOCTYPE html PUBLIC},
+ 'embedded and escaped xhtml';
};

subtest 'svg with javascript' => sub {
@@ -143,7 +166,7 @@ subtest 'list' => sub {
->json_hasnt('/prev')->json_has('/files/4')->json_has('/files/0/name')->json_has('/files/0/id')
->json_like('/files/0/saved', qr{^\d+-\d+-\d+T})->json_has('/files/0/size');
my @ids = map { $_->{id} } $files->();
- is @ids, 14, 'got all files';
+ is @ids, 16, 'got all files';

note 'unknown';
$t->get_ok('/api/files?limit=1&after=unknown')->status_is(200)->json_is('/files', [])

The developer of convos-chat patched this vulnerability by adding the logic to download the file when accessing the html file path as above.


Proof of Concept

1
2
3
4
5
6
7
1. Open the https://demo.convos.chat/login and Login as to above account
2. Go to https://demo.convos.chat/chat/irc-demo-irc-convos/<chat room name>
3. File Upload a html file
4. When you upload a file, an upload link is created in the comment form.
5. Please connect after attaching ".html" after the link

Video : https://www.youtube.com/watch?v=AfrsOY2S0Nc

Reporting Timeline

  • 2021-12-28 13h 20m : Reported this issue via the huntr
  • 2021-12-29 19h 38m : Validated this issue by jhthorsen
  • 2021-12-29 19h 41m : Patched this issue by jhthorsen
  • 2022-01-05 05h 28m : Assigned a CVE-2022-21650 via Github Staff

Reference