diff --git a/.travis.yml b/.travis.yml index 00c9de5..f94ef00 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,49 +2,59 @@ language: php sudo: false +addons: + postgresql: "9.6" + +services: + - mysql + cache: directories: - $HOME/.composer/cache php: - - 5.6 + - 7.1 env: - - DB=pgsql MOODLE_BRANCH=MOODLE_26_STABLE - - DB=pgsql MOODLE_BRANCH=MOODLE_27_STABLE - - DB=pgsql MOODLE_BRANCH=MOODLE_28_STABLE - - DB=pgsql MOODLE_BRANCH=MOODLE_29_STABLE - DB=pgsql MOODLE_BRANCH=MOODLE_30_STABLE - DB=pgsql MOODLE_BRANCH=MOODLE_31_STABLE - DB=pgsql MOODLE_BRANCH=MOODLE_32_STABLE + - DB=pgsql MOODLE_BRANCH=MOODLE_33_STABLE + - DB=pgsql MOODLE_BRANCH=MOODLE_34_STABLE + - DB=pgsql MOODLE_BRANCH=MOODLE_35_STABLE + - DB=pgsql MOODLE_BRANCH=MOODLE_36_STABLE + - DB=pgsql MOODLE_BRANCH=MOODLE_37_STABLE - DB=pgsql MOODLE_BRANCH=master - - DB=mysqli MOODLE_BRANCH=MOODLE_26_STABLE - - DB=mysqli MOODLE_BRANCH=MOODLE_27_STABLE - - DB=mysqli MOODLE_BRANCH=MOODLE_28_STABLE - - DB=mysqli MOODLE_BRANCH=MOODLE_29_STABLE - DB=mysqli MOODLE_BRANCH=MOODLE_30_STABLE - DB=mysqli MOODLE_BRANCH=MOODLE_31_STABLE - DB=mysqli MOODLE_BRANCH=MOODLE_32_STABLE + - DB=mysqli MOODLE_BRANCH=MOODLE_33_STABLE + - DB=mysqli MOODLE_BRANCH=MOODLE_34_STABLE + - DB=mysqli MOODLE_BRANCH=MOODLE_35_STABLE + - DB=mysqli MOODLE_BRANCH=MOODLE_36_STABLE + - DB=mysqli MOODLE_BRANCH=MOODLE_37_STABLE - DB=mysqli MOODLE_BRANCH=master matrix: include: - - php: 7.0 + - php: 5.5 + dist: trusty env: DB=pgsql MOODLE_BRANCH=MOODLE_30_STABLE - - php: 7.0 + - php: 5.5 + dist: trusty env: DB=pgsql MOODLE_BRANCH=MOODLE_31_STABLE - - php: 7.0 + - php: 5.6 + dist: trusty env: DB=pgsql MOODLE_BRANCH=MOODLE_32_STABLE - - php: 7.0 - env: DB=pgsql MOODLE_BRANCH=master - - php: 7.0 + - php: 5.5 + dist: trusty env: DB=mysqli MOODLE_BRANCH=MOODLE_30_STABLE - - php: 7.0 + - php: 5.5 + dist: trusty env: DB=mysqli MOODLE_BRANCH=MOODLE_31_STABLE - - php: 7.0 + - php: 5.6 + dist: trusty env: DB=mysqli MOODLE_BRANCH=MOODLE_32_STABLE - - php: 7.0 - env: DB=mysqli MOODLE_BRANCH=master before_install: - cd ../.. diff --git a/README.md b/README.md index 0ec058b..15bb9b5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Travis integration: [![Build Status](https://travis-ci.org/catalyst/moodle-local_csp.svg?branch=master)](https://travis-ci.org/catalyst/moodle-local_csp) +[![Build Status](https://travis-ci.org/catalyst/moodle-local_csp.svg?branch=master)](https://travis-ci.org/catalyst/moodle-local_csp) # moodle-local_csp * [Why would you want this?](#why-would-you-want-this) @@ -11,26 +11,36 @@ Why would you want this? ------------------------ Security, security, security. -This plugin helps you to detect and eliminate security errors in your Moodle such as: +This plugin helps you to detect and mitigate certain classes of security errors in your Moodle such as: + - Mixed content (https/http) after you switched to HTTPS. - - Same origin (or specified origin) policy for scripts and media data. + - Same origin (or specified origin) policy for scripts and media data. + - Unintended iframes What is this? ------------- -This plugin enables [Custom Security Policy headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) across the Moodle website. +This plugin allows you to easily test and rollout [Custom Security Policy headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) across your moodle. + Examples: - Report/enforce SSL origin for links, images etc. - Report/enforce same-origin for links, images etc. How does it work? ----------------- -Site admin configures CSP headers: Content-Security-Policy or Content-Security-Policy-Report-Only in the plugin settings. + +Site admin configures CSP headers: `Content-Security-Policy` or `Content-Security-Policy-Report-Only` in the plugin settings. Header Content-Security-Policy-Report-Only is for recording CSP violations in Moodle and reviewing them later from the plugin's report page. + Enabling of Content-Security-Policy blocks browser from showing site resources that violate defined rules. +CSP support in browsers is quite good: + +https://caniuse.com/#search=CSP + Installation ------------ Checkout or download the plugin source code into folder `local\csp` of your Moodle installation. + ```sh git clone git@github.com:catalyst/moodle-local_csp.git local\csp ``` @@ -42,19 +52,22 @@ unzip master.zip -d local/csp ``` Then go to your Moodle admin interface and complete installation and configuration. Example policy 'default-src https:;' will be reporting or enforcing the links to be HTTPS-only. Please note, the whole moodle website should be accessible via HTTPS for this to work. + For more examples of other CSP directives please read [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP). References ---------- -Relevant issue in Moodle tracker: (https://tracker.moodle.org/browse/MDL-46269) -A complementary plugin which works by searching the moodle DB for bad links: +See also: +Convert http embedded content to https on https sites where available +https://tracker.moodle.org/browse/MDL-46269 + +A complementary plugin which works by searching the moodle DB for bad links: https://github.com/moodlerooms/moodle-tool_httpsreplace This plugin was developed by Catalyst IT Australia: - https://www.catalyst-au.net/ Catalyst IT diff --git a/classes/table/csp_report.php b/classes/table/csp_report.php index cd61db6..94c8cc6 100644 --- a/classes/table/csp_report.php +++ b/classes/table/csp_report.php @@ -40,6 +40,30 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class csp_report extends \table_sql { + + /** + * Embeds a link to a drilldown table showing only 1 violation class + * + * @param stdObject $record fieldset object of db table with field timecreated + * @return string Link to drilldown table + */ + protected function col_failcounter($record) { + // Get blocked URI, and set as param for page if clicked on + $url = new \moodle_url('/local/csp/csp_report.php', array ('viewviolationclass' => $record->blockeduri)); + return \html_writer::link($url, $record->failcounter); + } + + /** + * Stop violateddirective from wrapping when long urls are present + * + * @param stdObject $record fieldset object of db table with field timecreated + * @return string Non breaking text line + */ + protected function col_violateddirective($record) { + // Stop line from wrapping + return \html_writer::tag('span', $record->violateddirective, array('style' => 'white-space: nowrap')); + } + /** * Formatting unix timestamps in column named timecreated to human readable time. * @@ -115,17 +139,57 @@ protected function col_blockeduri($record) { * @return string HTML link. */ protected function col_action($record) { - global $OUTPUT; - - $action = new \confirm_action(get_string('areyousuretodeleteonerecord', 'local_csp')); - $url = new \moodle_url($this->baseurl); - $url->params(array( - 'removerecordwithhash' => $record->sha1hash, - 'sesskey' => sesskey(), - 'redirecttopage' => $this->currpage, - )); - $actionlink = $OUTPUT->action_link($url, get_string('reset', 'local_csp'), $action); - - return $actionlink; + global $OUTPUT, $PAGE; + + // Find whether drilldown flag is present in PAGE params + $viewviolationclass = optional_param('viewviolationclass', false, PARAM_TEXT); + if ($viewviolationclass !== false) { + $action = new \confirm_action(get_string('areyousuretodeleteonerecord', 'local_csp')); + $url = new \moodle_url($this->baseurl); + $url->params(array( + 'removerecordwithid' => $record->id, + 'sesskey' => sesskey(), + 'redirecttopage' => $this->currpage, + )); + $actionlink = $OUTPUT->action_link($url, get_string('reset', 'local_csp'), $action); + + return $actionlink; + } else { + // Else delete entire violation class + $action = new \confirm_action(get_string('areyousuretodeleteonerecord', 'local_csp')); + $url = new \moodle_url($this->baseurl); + $url->params(array( + 'removeviolationclass' => $record->blockeduri, + 'sesskey' => sesskey(), + 'redirecttopage' => $this->currpage, + )); + $actionlink = $OUTPUT->action_link($url, get_string('reset', 'local_csp'), $action); + + return $actionlink; + } + } + + /** + * Gets the 3 highest violater documentURIs for each blockedURI + * + * @param stdObject $record fieldset object of db table with field timecreated + * @return string details of the highest violating documents + */ + protected function col_highestviolaters($record) { + global $DB, $CFG; + + // Get 3 highest violaters for each blocked URI + $sql = "SELECT * + FROM {local_csp} + WHERE blockeduri = ? + ORDER BY failcounter DESC"; + $violaters = $DB->get_records_sql($sql, array($record->blockeduri), 0, 3); + $return = ''; + foreach ($violaters as $violater) { + // Strip the top level domain out of the display + $urlstring = str_replace($CFG->wwwroot, '', $violater->documenturi); + $return .= get_string('highestviolaterscount', 'local_csp', $violater->failcounter).' '.\html_writer::link($violater->documenturi, $urlstring).'
'; + } + return $return; } } // end class csp_report diff --git a/collector.php b/collector.php index 024ca06..f3644b1 100644 --- a/collector.php +++ b/collector.php @@ -22,11 +22,11 @@ * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +require_once(__DIR__ . '/../../config.php'); $inputjson = file_get_contents('php://input'); $cspreport = json_decode($inputjson, true)['csp-report']; -require_once(__DIR__ . '/../../config.php'); global $DB; if ($cspreport) { @@ -50,8 +50,9 @@ echo 'Repeated CSP violation, failcounter incremented.'; } else { // Insert a new record. - $dataobject->documenturi = $documenturi; - $dataobject->blockeduri = $blockeduri; + // Truncate URIs of extreme length. + $dataobject->documenturi = substr($documenturi, 0, 1024); + $dataobject->blockeduri = substr($blockeduri, 0, 1024); $dataobject->violateddirective = $cspreport['violated-directive']; $dataobject->timecreated = time(); $dataobject->sha1hash = $hash; diff --git a/csp_report.php b/csp_report.php index 5b25985..d682e52 100644 --- a/csp_report.php +++ b/csp_report.php @@ -28,9 +28,18 @@ global $DB; -// Remove CSP report record with specified hash. This is triggered from \local_csp\table\csp_report->col_action(). -if (($removerecordwithhash = optional_param('removerecordwithhash', false, PARAM_TEXT)) !== false && confirm_sesskey()) { - $DB->delete_records('local_csp', array('sha1hash' => $removerecordwithhash)); +// Delete violation class if param set +if (($removeviolationclass = optional_param('removeviolationclass', false, PARAM_TEXT)) !== false && confirm_sesskey()) { + $DB->delete_records('local_csp', array('blockeduri' => $removeviolationclass)); + $PAGE->set_url('/local/csp/csp_report.php', array( + 'page' => optional_param('redirecttopage', 0, PARAM_INT), + )); + redirect($PAGE->url); +} + +// Delete individual violation records if set +if (($removerecordwithid = optional_param('removerecordwithid', false, PARAM_TEXT)) !== false && confirm_sesskey()) { + $DB->delete_records('local_csp', array('id' => $removerecordwithid)); $PAGE->set_url('/local/csp/csp_report.php', array( 'page' => optional_param('redirecttopage', 0, PARAM_INT), )); @@ -63,11 +72,11 @@ echo $OUTPUT->single_button($urlresetallcspstatistics, get_string('resetallcspstatistics', 'local_csp'), 'post', array('actions' => array($action))); -$documenturi = get_string('documenturi', 'local_csp'); $blockeduri = get_string('blockeduri', 'local_csp'); +$highestviolaters = get_string('highestviolaters', 'local_csp'); $violateddirective = get_string('violateddirective', 'local_csp'); +$documenturi = get_string('documenturi', 'local_csp'); $failcounter = get_string('failcounter', 'local_csp'); -$timecreated = get_string('timecreated', 'local_csp'); $timeupdated = get_string('timeupdated', 'local_csp'); $action = get_string('action', 'local_csp'); @@ -76,27 +85,62 @@ $table->sortable(true, 'failcounter', SORT_DESC); $table->define_columns(array( 'failcounter', - 'documenturi', - 'blockeduri', 'violateddirective', + 'blockeduri', + 'highestviolaters', 'timecreated', - 'timeupdated', 'action', )); $table->define_headers(array( $failcounter, - $documenturi, - $blockeduri, $violateddirective, - $timecreated, + $blockeduri, + $highestviolaters, $timeupdated, $action, )); -$fields = 'id, sha1hash, documenturi, blockeduri, violateddirective, failcounter, timecreated, timeupdated'; -$from = '{local_csp}'; -$where = '1 = 1'; -$table->set_sql($fields, $from, $where); +$viewviolationclass = optional_param('viewviolationclass', false, PARAM_TEXT); +// If user has clicked on a violation to view all violation entries +if ($viewviolationclass !== false) { + $fields = 'id, sha1hash, blockeduri, violateddirective, failcounter, timeupdated, documenturi'; + $from = "{local_csp}"; + $where = "blockeduri = ?"; + $params = array($viewviolationclass); + + // Redefine columns to display Violation source + $table->define_columns(array( + 'failcounter', + 'violateddirective', + 'blockeduri', + 'documenturi', + 'timeupdated', + 'action', + )); + $table->define_headers(array( + $failcounter, + $violateddirective, + $blockeduri, + $documenturi, + $timeupdated, + $action, + )); + +} else { + $fields = 'id, blockeduri, violateddirective, failcounter, timecreated'; + // Select the first blockedURI of a type, and collapse the rest while summing failcounter + // + $from = "(SELECT MAX(id) AS id, + blockeduri, + violateddirective, + SUM(failcounter) AS failcounter, + MAX(timecreated) AS timecreated + FROM {local_csp} + GROUP BY blockeduri, violateddirective) AS A"; + $where = '1 = 1'; + $params = array(); +} +$table->set_sql($fields, $from, $where, $params); $table->out(30, true); diff --git a/db/install.xml b/db/install.xml index cb27452..7c2018b 100755 --- a/db/install.xml +++ b/db/install.xml @@ -1,5 +1,5 @@ - @@ -7,8 +7,8 @@ - - + + diff --git a/db/upgrade.php b/db/upgrade.php new file mode 100644 index 0000000..6d14c5c --- /dev/null +++ b/db/upgrade.php @@ -0,0 +1,54 @@ +. + +/** + * Upgrade script + * + * @package local_csp + * @author Peter Burnett + * @copyright Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +function xmldb_local_csp_upgrade($oldversion) { + global $DB; + $dbman = $DB->get_manager(); + + if ($oldversion < 2019100100) { + + // Changing precision of field documenturi on table local_csp to (1333). + $table = new xmldb_table('local_csp'); + $field = new xmldb_field('documenturi', XMLDB_TYPE_CHAR, '1333', null, null, null, null, 'id'); + + // Launch change of precision for field documenturi. + $dbman->change_field_precision($table, $field); + + // Changing precision of field blockeduri on table local_csp to (1333). + $table = new xmldb_table('local_csp'); + $field = new xmldb_field('blockeduri', XMLDB_TYPE_CHAR, '1333', null, null, null, null, 'documenturi'); + + // Launch change of precision for field blockeduri. + $dbman->change_field_precision($table, $field); + + // Csp savepoint reached. + upgrade_plugin_savepoint(true, 2019100100, 'local', 'csp'); + } + + return true; +} + diff --git a/lang/en/local_csp.php b/lang/en/local_csp.php index b8f489a..fb9bd97 100644 --- a/lang/en/local_csp.php +++ b/lang/en/local_csp.php @@ -45,7 +45,9 @@ $string['cspsettings'] = 'Content security policy settings'; $string['cspsettingsinfo'] = '

CSP works through adding a special HTTP response header to every Moodle page. Modern browsers, when they see this header, are able to perform certain actions e.g. block insecure content on such pages. Please read more about CSP here.

If you leave any of these settings blank CSP headers will not be used.

'; $string['documenturi'] = 'Violation at'; -$string['failcounter'] = 'Count'; +$string['failcounter'] = '#'; +$string['highestviolaters'] = 'Top Violation Sources'; +$string['highestviolaterscount'] = 'Count: {$a}'; $string['loadingmixedcontentdescription'] = 'When accessing moodle website via HTTPS browser prohibits displaying of the below resources because they origin from HTTP.
You should be able to see it in your browser\'s Javascript console.'; $string['loadinsecurecss'] = 'Load insecure css from {$a}'; $string['loadinsecureiframe'] = 'Load insecure iframe from {$a}'; @@ -58,7 +60,6 @@ $string['reset'] = 'Reset'; $string['resetallcspstatistics'] = 'Reset all statistics'; $string['scspheadernone'] = 'Not used'; -$string['timecreated'] = 'Recorded'; $string['timeupdated'] = 'Last'; $string['violateddirective'] = 'Policy'; $string['privacy:metadata'] = 'The CSP plugin contains no user specific data.'; diff --git a/samples/sample.html b/samples/sample.html index efc9aa1..e0656fc 100644 --- a/samples/sample.html +++ b/samples/sample.html @@ -7,4 +7,4 @@

Successfully loaded sample.html!

- \ No newline at end of file + diff --git a/version.php b/version.php index 7ce351c..940a2be 100644 --- a/version.php +++ b/version.php @@ -25,8 +25,8 @@ defined('MOODLE_INTERNAL') || die; -$plugin->version = 2017041801; -$plugin->release = 2017041801; +$plugin->version = 2019100100; +$plugin->release = 2019100100; $plugin->requires = 2015051100; $plugin->maturity = MATURITY_STABLE; $plugin->component = 'local_csp';