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

Fix/1920 csp #2003

Merged
merged 11 commits into from
Oct 26, 2023
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ log
rvd_front.conf
pkg-debian-out
public/img/screenshots
public/js/custom
yarn.lock
node_modules/
t/vm/b10*
Expand Down
3 changes: 3 additions & 0 deletions etc/rvd_front.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,7 @@
# Insert widget in /js/custom/insert_here_widget.js
# this widget embed js in templates/bootstrap/scripts.html.ep
,widget => ''
# Content-Security-Policy HTTP response header helps you reduce XSS risks
# define custom directives. More info https://content-security-policy.com/
,security_policy => 'foo.bar.com'
};
53 changes: 53 additions & 0 deletions lib/Ravada.pm
Original file line number Diff line number Diff line change
Expand Up @@ -2543,6 +2543,59 @@ sub _sql_insert_defaults($self){
,name => 'auto_view'
,value => $conf->{auto_view}
}
,{ id_parent => $id_frontend
,name => "widget"
}
,{
id_parent => $id_frontend
,name => 'content_security_policy'
}
,{
id_parent => "/frontend/content_security_policy"
,name => "all"
,value => ''
}
,{
id_parent => "/frontend/content_security_policy"
,name => "default-src"
,value => ''
}
,{
id_parent => "/frontend/content_security_policy"
,name => "style-src"
,value => ''
}
,{
id_parent => "/frontend/content_security_policy"
,name => "script-src"
,value => ''
}
,{
id_parent => "/frontend/content_security_policy"
,name => "object-src"
,value => ''
}
,{
id_parent => "/frontend/content_security_policy"
,name => "frame-src"
,value => ''
}
,{
id_parent => "/frontend/content_security_policy"
,name => "font-src"
,value => ''
}

,{
id_parent => "/frontend/content_security_policy"
,name => "connect-src"
,value => ''
}
,{
id_parent => "/frontend/content_security_policy"
,name => "media-src"
,value => ''
}
,{
id_parent => $id_backend
,name => 'start_limit'
Expand Down
19 changes: 17 additions & 2 deletions lib/Ravada/Front.pm
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use Hash::Util qw(lock_hash);
use IPC::Run3 qw(run3);
use JSON::XS;
use Moose;
use Storable qw(dclone);
use Ravada;
use Ravada::Auth::LDAP;
use Ravada::Front::Domain;
Expand Down Expand Up @@ -1481,6 +1482,19 @@ sub _settings_by_id($self) {
return $orig_settings;
}

sub _settings_by_parent($self,$parent) {
my $data = $self->_setting_data($parent);
my $sth = $self->_dbh->prepare("SELECT name,value FROM settings "
." WHERE id_parent = ? ");
$sth->execute($data->{id});
my $ret;
while (my ($name, $value) = $sth->fetchrow) {
$value = '' if !defined $value;
$ret->{$name} = $value;
}
return $ret;
}

=head2 feature

Returns if a feature is available
Expand Down Expand Up @@ -1521,9 +1535,10 @@ sub update_settings_global($self, $arg, $user, $reload, $orig_settings = $self->
confess Dumper([$field,$arg->{$field}]) if !ref($arg->{$field});
if ( scalar(keys %{$arg->{$field}})>2 ) {
confess if !keys %{$arg->{$field}};
$self->update_settings_global($arg->{$field}, $user, $reload, $orig_settings);
my $field2 = dclone($arg->{$field});
$self->update_settings_global($field2, $user, $reload, $orig_settings);
}
confess "Error: invalid field $field" if $field !~ /^\w+$/;
confess "Error: invalid field $field" if $field !~ /^\w[\w\-]+$/;
my ( $value, $id )
= ($arg->{$field}->{value}
, $arg->{$field}->{id}
Expand Down
25 changes: 24 additions & 1 deletion public/js/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -1453,7 +1453,27 @@ ravadaApp.directive("solShowMachine", swMach)

function settings_global_ctrl($scope, $http) {
$scope.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
$scope.init = function() {
$scope.csp_locked = false;
$scope.set_csp_locked=function() {
var keys = Object.keys($scope.settings.frontend.content_security_policy);
var found = 0;
for ( var n_key=0 ; n_key<keys.length ; n_key++) {
var field=keys[n_key];
if ( field != 'all' && field != 'id' && field != 'value'
&& $scope.settings.frontend.content_security_policy[field].value) {
found++;
}
}
$scope.csp_locked = found>0;
if ($scope.csp_locked && !$scope.csp_advanced) {
$scope.csp_advanced = true;
}
};
$scope.init = function(url, csp_advanced) {
$scope.csp_advanced=false;
if (csp_advanced) {
$scope.csp_advanced=true;
}
$http.get('/settings_global.json').then(function(response) {
$scope.settings = response.data;
var now = new Date();
Expand All @@ -1472,17 +1492,20 @@ ravadaApp.directive("solShowMachine", swMach)
$scope.settings.frontend.maintenance_end.value
=new Date($scope.settings.frontend.maintenance_end.value);
}
$scope.set_csp_locked();
});
};
$scope.load_settings = function() {
$scope.init();
$scope.set_csp_locked();
$scope.formSettings.$setPristine();
};
$scope.update_settings = function() {
$scope.formSettings.$setPristine();
$http.post('/settings_global'
,JSON.stringify($scope.settings)
).then(function(response) {
$scope.set_csp_locked();
if (response.data.reload) {
window.location.reload();
}
Expand Down
48 changes: 41 additions & 7 deletions script/rvd_front
Original file line number Diff line number Diff line change
Expand Up @@ -147,21 +147,50 @@ sub _time() {
return strftime('%Y/%m/%d:%H:%M:%S %z',localtime);
}

sub _security_policy() {

my $config=$RAVADA->_settings_by_parent("/frontend/content_security_policy");
my $all = ($config->{all} or '');
my %src = (
"default-src" => "'self' sha256- sha384 http: https: data:"
,"style-src" => "'self' cdnjs.cloudflare.com stackpath.bootstrapcdn.com cdn.ckeditor.com cdn.jsdelivr.net use.fontawesome.com 'unsafe-inline'"
,"script-src" => "'self' code.jquery.com cdn.ckeditor.com cdnjs.cloudflare.com stackpath.bootstrapcdn.com ajax.googleapis.com cdn.jsdelivr.net 'unsafe-inline' 'unsafe-eval'"
,"object-src" => " 'self'"
,"media-src" => "'self'"
,"frame-src" => "'self'"
,"font-src" => "'self' data: use.fontawesome.com"
,"connect-src" => "'self'"
);
my $sec = '';
for my $field (sort keys %src) {
$sec .= " " if $sec;
$sec .= $field." ".$src{$field};
$sec .= " $all" if $all;
if ( exists $config->{$field} ) {
$sec .= " ".${config}->{$field};
}
$field =~ s/-/_/g;
if ( exists $config->{$field} ) {
$sec .= " ".$config->{$field};
}

$sec .= ";";
}
return $sec;
}

hook before_routes => sub {
my $c = shift;

$c ->res->headers->content_security_policy (
"object-src 'auto';"
."media-src 'self';"
." frame-src 'self';"
." connect-src 'self'; ");

my $sec = _security_policy();
$c ->res->headers->content_security_policy($sec);

$USER = undef;

$c->stash(version => $RAVADA->version);
my $url = $c->req->url->to_abs->path;
my $host = $c->req->url->to_abs->host;
my $widget=( $CONFIG_FRONT->{widget} or $RAVADA->setting('/frontend/widget'));
$c->stash(css=>['/css/sb-admin.css']
,js_mod=>[ ## angular modules
'/js/booking/booking.module.js?v='.$RAVADA->version
Expand All @@ -184,7 +213,7 @@ hook before_routes => sub {
,host => $host
,bookings => $RAVADA->setting('/backend/bookings')
,FEATURE => {}
,widget => $CONFIG_FRONT->{widget}
,widget => $widget
);

$USER = _logged_in($c);
Expand Down Expand Up @@ -2810,6 +2839,11 @@ sub admin {
return access_denied($c) unless $USER->is_admin;
my $url = $c->req->url->to_abs->path;
my $host = $c->req->url->to_abs->host;
my $csp = $RAVADA->_settings_by_parent("/frontend/content_security_policy");
my $csp_advanced = 0;
$csp_advanced = grep $csp->{$_}, grep /-/,keys %$csp;

$c->stash( csp => $csp , csp_advanced => $csp_advanced);
$c->stash(url_login => "/login");
}
if ($page eq 'storage') {
Expand Down
85 changes: 85 additions & 0 deletions t/mojo/40_security_policy.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use warnings;
use strict;

use Carp qw(confess);
use Data::Dumper;
use HTML::Lint;
use Test::More;
use Test::Mojo;
use Mojo::File 'path';
use Mojo::JSON qw(decode_json);
use Storable qw(dclone);

use lib 't/lib';
use Test::Ravada;

no warnings "experimental::signatures";
use feature qw(signatures);

my $SECONDS_TIMEOUT = 15;

my $t;

my $URL_LOGOUT = '/logout';
my ($USERNAME, $PASSWORD) = (user_admin->name, "$$ $$");
my $SCRIPT = path(__FILE__)->dirname->sibling('../script/rvd_front');

$ENV{MOJO_MODE} = 'devel';
init('/etc/ravada.conf',0);
my $connector = rvd_back->connector;
like($connector->{driver} , qr/mysql/i) or BAIL_OUT;

$Test::Ravada::BACKGROUND=1;

$t = Test::Mojo->new($SCRIPT);
$t->ua->inactivity_timeout(900);
$t->ua->connect_timeout(60);

mojo_login($t, $USERNAME, $PASSWORD);

my $sth = rvd_front->_dbh->prepare("UPDATE settings set value='' WHERE id_parent=?");

$t->get_ok("/settings_global.json")->status_is(200);
my $body = $t->tx->res->body();
my $settings = decode_json($body);

$sth->execute($settings->{frontend}->{content_security_policy}->{id});

my $new = dclone($settings);
my $exp_default = "foodefault.example.com";
my $exp_all = "fooall.example.com";
$new->{frontend}->{content_security_policy}->{'default-src'}->{value} = $exp_default;
$new->{frontend}->{content_security_policy}->{'all'}->{value} = $exp_all;
delete $new->{backend};

my $reload=0;
rvd_front->update_settings_global($new,user_admin,$reload);

$t->post_ok("/settings_global", json => $new );

$t->get_ok("/settings_global.json")->status_is(200);
$body = $t->tx->res->body();
my $settings2 = decode_json($body);
is($settings2->{frontend}->{content_security_policy}->{'all'}->{value} , $exp_all) or exit;
is($settings2->{frontend}->{content_security_policy}->{'default-src'}->{value} , $exp_default) or exit;

my $config_csp = rvd_front->_settings_by_parent("/frontend/content_security_policy");
is($config_csp->{all}, $exp_all);
is($config_csp->{'default-src'}, $exp_default);

my $header = $t->tx->res->headers->content_security_policy();
my %csp;
for my $entry (split /;/,$header) {
my ($key,$value) = $entry =~ /\s*(.*?)\s+(.*)/;
$csp{$key}=$value;
}

like($csp{'default-src'},qr/$exp_all/);
like($csp{'default-src'},qr/$exp_default/);

$sth->execute($settings->{frontend}->{content_security_policy}->{id});

$new->{frontend}->{content_security_policy}->{'default-src'}->{value} = '';
$new->{frontend}->{content_security_policy}->{'all'}->{value} = '';
$t->post_ok("/settings_global", json => $new );
done_testing();
2 changes: 1 addition & 1 deletion templates/bootstrap/header.html.ep
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<title>Ravada VDI</title>

% if ( !$fallback ) {
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css">
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/ui-bootstrap-csp.css">
<link href="https://use.fontawesome.com/releases/v5.0.7/css/all.css" rel="stylesheet">
Expand Down
Loading
Loading