diff --git a/Dockerfile b/Dockerfile index 77837fc..f8db9e0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,45 @@ FROM ubuntu:latest -RUN apt-get update -RUN apt-get install -y python-pip -RUN apt-get install -y apache2 -RUN pip install -U pip -RUN pip install -U flask -RUN pip install -U flask-cors -RUN echo "ServerName localhost " >> /etc/apache2/apache2.conf -RUN echo "$user hard nproc 20" >> /etc/security/limits.conf + +ENV MYSQL_USER=root \ + MYSQL_PASSWORD= \ + MYSQL_HOST=localhost \ + MYSQL_DATABASE=GROUP5_SECRET_DIARY \ + TZ=Asia/Singapore + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN apt-get update \ + && apt-get install -y python-pip \ + && apt-get install -y apache2 + +ADD ./src/db /tmp/ + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y mysql-server && \ + rm -rf /var/lib/apt/lists/* && \ + sed -i 's/^\(bind-address\s.*\)/# \1/' /etc/mysql/my.cnf && \ + sed -i 's/^\(log_error\s.*\)/# \1/' /etc/mysql/my.cnf && \ + service mysql start && \ + mysql -u root -e "CREATE DATABASE IF NOT EXISTS GROUP5_SECRET_DIARY CHARACTER SET utf8 COLLATE utf8_general_ci; FLUSH PRIVILEGES;" && \ + mysql -u root -e "SHOW DATABASES" && \ + mysql -u root "GROUP5_SECRET_DIARY" < "/tmp/schema.sql" + +RUN mkdir -p /var/run/mysqld && chown mysql:mysql /var/run/mysqld +VOLUME ["/etc/mysql", "/var/lib/mysql"] +WORKDIR /data + +RUN apt-get update && \ + apt-get install -y libmysqlclient-dev + +RUN pip install -U pip flask flask-cors Flask-SQLAlchemy MySQL-python flask-bcrypt flask-marshmallow marshmallow-sqlalchemy + +RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf +RUN echo "$user hard nproc 20" >> /etc/security/limits.conf + ADD ./src/service /service ADD ./src/html /var/www/html + EXPOSE 80 EXPOSE 8080 + CMD ["/bin/bash", "/service/start_services.sh"] diff --git a/README.md b/README.md index 9a814e3..c8f1cac 100644 --- a/README.md +++ b/README.md @@ -88,56 +88,65 @@ If a response is received, you're good to go. Please replace the example screenshots with screenshots of your completed project. Feel free to include more than one. - + ## Administration and Evaluation Please fill out this section with details relevant to your team. -### Team Members +### Team Members (Group 5) -1. Member 1 Name -2. Member 2 Name -3. Member 3 Name -4. Member 4 Name +1. Member 1 Hu Shuchen +2. Member 2 Wang Zhuochun +3. Member 3 Arvee Vergara Valmores ### Short Answer Questions #### Question 1: Briefly describe the web technology stack used in your implementation. -Answer: Please replace this sentence with your answer. +Answer: We have used Flask web framework, MySQL for database schema, Vue.js for front-end development. #### Question 2: Are there any security considerations your team thought about? -Answer: Please replace this sentence with your answer. +Answer: We have enforced user password strength with minimum length of 8 characters, containing both small and capitals letters as well as at one number. We have also implemented account lockout policy where the user account will be locked for 1 hour after 3 failed login attempts. +As we are using MySQL for our database, we have also made use of the mature library SQLAlchemy, and utilized prepared statements in the backend codes to enforce our protection against SQL injection attacks. #### Question 3: Are there any improvements you would make to the API specification to improve the security of the web application? -Answer: Please replace this sentence with your answer. +Answer: - Enforce authentication token expiry for certain duration or inactivity. This can reduce the risk of stolen tokens through XSS or other types of attacks. + - Allow password reset or change of password with security measures + - Implement 2-factor or multi-factor user authentication + - Implement HTTPS to encrypt JSON requests and responses over the network #### Question 4: Are there any additional features you would like to highlight? -Answer: Please replace this sentence with your answer. +Answer: Not other than the password strength and account lockout policy. Password requirements are shown in the user interface as well when users try to set their passwords. #### Question 5: Is your web application vulnerable? If yes, how and why? If not, what measures did you take to secure it? -Answer: Please replace this sentence with your answer. +Answer: We did not find our application vulnerable. In our development, we have made sure to use of mature libraries with functions to render user inputs like their diaries just as text rather than scripts. We have tested possibilities of cross-site scripting as shown in the screenshot below and our application is not vulnerable to those. + + + #### Feedback: Is there any other feedback you would like to give? -Answer: Please replace this sentence with your answer. +Answer: No. ### Declaration #### Please declare your individual contributions to the assignment: -1. Member 1 Name - - Integrated feature x into component y - - Implemented z -2. Member 2 Name - - Wrote the front-end code -3. Member 3 Name - - Designed the database schema -4. Member 4 Name - - Implemented x +1. Member 1 Hu Shuchen + - Wrote the front-end code and integrated with the API features developed by teammates + - Tested and checked the work done by teammates + - Logistics (set up Github organization, README documentation & short answer questions) +2. Member 2 Wang Zhuochun + - Developed the Users API + - Designed the database schema for Users + - Tested and checked the work done by teammates +3. Member 3 Arvee Vergara Valmores + - Developed the Diary API + - Designed the database schema for Diary entries + - Tested and checked the work done by teammates diff --git a/img/XSS test.png b/img/XSS test.png new file mode 100644 index 0000000..5f3195b Binary files /dev/null and b/img/XSS test.png differ diff --git a/img/group5.png b/img/group5.png new file mode 100644 index 0000000..f42b0d7 Binary files /dev/null and b/img/group5.png differ diff --git a/init.sh b/init.sh new file mode 100755 index 0000000..bdc2f83 --- /dev/null +++ b/init.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +if [ -n "$MYSQL_PASSWORD" ] ; then + + TEMP_FILE='/service/schema.sql' + cat > "$TEMP_FILE" <<-EOSQL + DELETE FROM mysql.user WHERE user = 'root' AND host = '%'; + CREATE USER 'root'@'%' IDENTIFIED BY '${MYSQL_PASSWORD}' ; + GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION ; + FLUSH PRIVILEGES ; + EOSQL + + # set this as an init-file to execute on startup + set -- "$@" --init-file="$TEMP_FILE" +fi + +# execute the command supplied +exec "$@" \ No newline at end of file diff --git a/run-mac.sh b/run-mac.sh new file mode 100755 index 0000000..4a4dff2 --- /dev/null +++ b/run-mac.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +docker kill $(docker ps -q) +docker rm $(docker ps -a -q) +docker build . -t cs5331 +docker run -p 1888:80 -p 8080:8080 -t cs5331 diff --git a/src/db/.DS_Store b/src/db/.DS_Store new file mode 100644 index 0000000..f90dd29 Binary files /dev/null and b/src/db/.DS_Store differ diff --git a/src/db/init_db.sh b/src/db/init_db.sh new file mode 100755 index 0000000..d2014f2 --- /dev/null +++ b/src/db/init_db.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +service mysql start + +sleep 3 + +mysql -u root -e "CREATE DATABASE IF NOT EXISTS GROUP5_SECRET_DIARY CHARACTER SET utf8 COLLATE utf8_general_ci; FLUSH PRIVILEGES;" +mysql -u root -e "SHOW DATABASES" +mysql -u root "GROUP5_SECRET_DIARY" < "/tmp/schema.sql" +mysql -u root -e "USE GROUP5_SECRET_DIARY; SHOW TABLES" \ No newline at end of file diff --git a/src/db/schema.sql b/src/db/schema.sql new file mode 100644 index 0000000..6dc031a --- /dev/null +++ b/src/db/schema.sql @@ -0,0 +1,39 @@ +# Create a separate user from root, if necessary +# CREATE USER 'new_user'@'%' IDENTIFIED BY PASSWORD 'password'; +# Grant privileges on new_user +# GRANT ALL PRIVILEGES ON cs5331_secret_diary.* TO 'new_user'@'%' WITH GRANT OPTION; + +DROP TABLE IF EXISTS `user`; + +CREATE TABLE `user` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `username` VARCHAR(255) COLLATE utf8_unicode_ci NOT NULL, + `encrypted_password` VARCHAR(255) COLLATE utf8_unicode_ci NOT NULL, + `fullname` VARCHAR(255) COLLATE utf8_unicode_ci NOT NULL, + `age` INT(11) NOT NULL DEFAULT 0, + `sign_in_count` INT(11) NOT NULL DEFAULT 0, + `locked_at` TIMESTAMP NULL DEFAULT NULL, + `session_token` VARCHAR(255) COLLATE utf8_unicode_ci NOT NULL, + `session_created_at` TIMESTAMP NULL DEFAULT NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `index_user_on_username` (`username`), + UNIQUE KEY `index_user_on_session_token` (`session_token`) +) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + + +DROP TABLE IF EXISTS `diary`; + +CREATE TABLE `diary` ( + `id` INT NOT NULL AUTO_INCREMENT, + `author` VARCHAR(255) COLLATE utf8_unicode_ci NOT NULL, + `publish_date` TIMESTAMP NULL DEFAULT NULL, + `title` VARCHAR(100) COLLATE utf8_unicode_ci, + `text` TEXT COLLATE utf8_unicode_ci, + `public` BOOL NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `index_user_on_id` (`id`) +) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + + diff --git a/src/html-src/.gitignore b/src/html-src/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/src/html-src/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/src/html-src/gulpfile.js b/src/html-src/gulpfile.js new file mode 100644 index 0000000..f325036 --- /dev/null +++ b/src/html-src/gulpfile.js @@ -0,0 +1,40 @@ +var gulp = require("gulp"); +var pug = require("gulp-pug"); +var uglify = require('gulp-uglify'); +var livereload = require('gulp-livereload'); +var http = require('http'); +var st = require('st'); + +gulp.task("html", function() { + gulp.src("pages/*.pug") + .pipe(pug()) + .pipe(gulp.dest("../html/")) + .pipe(livereload()); +}); + +gulp.task("js", function() { + gulp.src("js/*.js") + .pipe(uglify()) + .pipe(gulp.dest("../html/js")) + .pipe(livereload()); +}); + +gulp.task("server", function(done) { + http.createServer( + st({ + path: "../html", + cache: false + }) + ).listen(1889, done); +}); + +gulp.task("watch", ["default", "server"], function() { + livereload.listen({ + basePath: "../html/" + }); + + gulp.watch(["layouts/**/*.pug", "pages/**/*.pug"], ["html"]); + gulp.watch(["js/**/*.js"], ["js"]); +}); + +gulp.task("default", ["js", "html"]); diff --git a/src/html-src/js/common.js b/src/html-src/js/common.js new file mode 100644 index 0000000..883c048 --- /dev/null +++ b/src/html-src/js/common.js @@ -0,0 +1,204 @@ +var HOST = "http://localhost:8080"; + +$(function() { + + $.ajaxSetup({ + contentType: "application/json" + }); + + // Navbar + var navbar = new Vue({ + el: "#navbarToggler", + data: { + loggedIn: false, + user: {}, + items: [{ + "name": "Home", + "url": "index.html", + "active": false, + "annoymous": true, + "login": true + }, + { + "name": "My Diary", + "url": "mydiary.html", + "active": false, + "annoymous": false, + "login": true + }, + { + "name": "New Diary", + "url": "newdiary.html", + "active": false, + "annoymous": false, + "login": true + }, + { + "name": "Register", + "url": "register.html", + "active": false, + "annoymous": true, + "login": false + }, + { + "name": "Login", + "url": "login.html", + "active": false, + "annoymous": true, + "login": false + } + ] + }, + methods: { + menu: function(items) { + var menuItems = [], + url = location.pathname; + + for (var i = 0, l = items.length; i < l; i++) { + var v = items[i]; + + if (this.loggedIn && v.login) { + menuItems.push(v); + } else if (!this.loggedIn && v.annoymous) { + menuItems.push(v); + } + + v.active = (url == "/" + v.url); + } + + return menuItems; + }, + logout: function() { + $.post(HOST + "/users/expire", JSON.stringify({ + "token": sessionStorage.getItem("token") + }), function(data) { + if (data.status) { + sessionStorage.setItem("message", "You have logged out!"); + } + + location.href = "index.html"; + }); + } + } + }); + + var greeting = new Vue({ + el: "#greeting", + data: { + loggedIn: false, + user: {}, + message: sessionStorage.getItem("message") + } + }); + + if (sessionStorage.getItem("message")) { + sessionStorage.removeItem("message"); + setTimeout(function() { greeting.message = undefined; }, 10000); + } + + if (sessionStorage.getItem("token")) { + $.post(HOST + "/users", JSON.stringify({ + "token": sessionStorage.getItem("token") + }), function(data) { + if (data.status) { + navbar.loggedIn = true; + navbar.user = data.result; + + greeting.loggedIn = true; + greeting.user = data.result; + } else { + sessionStorage.removeItem("token"); + } + }); + } + + // Health check + var healthCheck = new Vue({ + el: "#health-check", + data: { + status: false + } + }); + + $.getJSON(HOST + "/meta/heartbeat", function(data) { + healthCheck.status = !!data.status; + }); + + // Maintainers + var maintainers = new Vue({ + el: "#maintainers", + data: { + people: [] + } + }); + + $.getJSON(HOST + "/meta/members", function(data) { + if (!!data.status) { + maintainers.people = data.result; + } + }); + + // Dairy + // + // { + // "id": 1, + // "title": "My First Project", + // "author": "ashrugged", + // "publish_date": "2013-02-27T13:37:00+00:00", + // "public": true, + // "text": "If you don't know, the thing to do is not to get scared, but to learn." + // } + // + Vue.component("diary-entry", { + props: ["diary", "editable"], + data: function() { + return { + error: undefined + }; + }, + template: '
{{ diary.text }}
' + + 'Requirements for the front end are as follows:
-{{ diary.text }}
`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Remove the bottom border in Firefox 39-.\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Duplicate behavior to the data-* attribute for our tooltip plugin\n\nabbr[title],\nabbr[data-original-title] { // 4\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 1\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\ndfn {\n font-style: italic; // Add the correct font style in Android 4.3-\n}\n\n// stylelint-disable font-weight-notation\nb,\nstrong {\n font-weight: bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n// stylelint-enable font-weight-notation\n\nsmall {\n font-size: 80%; // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n -webkit-text-decoration-skip: objects; // Remove gaps in links underline in iOS 8+ and Safari 8+.\n\n @include hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href)\n// which have not been made explicitly keyboard-focusable (without tabindex).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n\n @include hover-focus {\n color: inherit;\n text-decoration: none;\n }\n\n &:focus {\n outline: 0;\n }\n}\n\n\n//\n// Code\n//\n\n// stylelint-disable font-family-no-duplicate-names\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace; // Correct the inheritance and scaling of font size in all browsers.\n font-size: 1em; // Correct the odd `em` font sizing in all browsers.\n}\n// stylelint-enable font-family-no-duplicate-names\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n // We have @viewport set which causes scrollbars to overlap content in IE11 and Edge, so\n // we force a non-overlapping, non-auto-hiding scrollbar to counteract.\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg:not(:root) {\n overflow: hidden; // Hide the overflow in IE\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $text-muted;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `
`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Remove the bottom border in Firefox 39-.\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Duplicate behavior to the data-* attribute for our tooltip plugin\n\nabbr[title],\nabbr[data-original-title] { // 4\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 1\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\ndfn {\n font-style: italic; // Add the correct font style in Android 4.3-\n}\n\n// stylelint-disable font-weight-notation\nb,\nstrong {\n font-weight: bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n// stylelint-enable font-weight-notation\n\nsmall {\n font-size: 80%; // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n -webkit-text-decoration-skip: objects; // Remove gaps in links underline in iOS 8+ and Safari 8+.\n\n @include hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href)\n// which have not been made explicitly keyboard-focusable (without tabindex).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n\n @include hover-focus {\n color: inherit;\n text-decoration: none;\n }\n\n &:focus {\n outline: 0;\n }\n}\n\n\n//\n// Code\n//\n\n// stylelint-disable font-family-no-duplicate-names\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace; // Correct the inheritance and scaling of font size in all browsers.\n font-size: 1em; // Correct the odd `em` font sizing in all browsers.\n}\n// stylelint-enable font-family-no-duplicate-names\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n // We have @viewport set which causes scrollbars to overlap content in IE11 and Edge, so\n // we force a non-overlapping, non-auto-hiding scrollbar to counteract.\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg:not(:root) {\n overflow: hidden; // Hide the overflow in IE\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $text-muted;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `
`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Remove the bottom border in Firefox 39-.\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Duplicate behavior to the data-* attribute for our tooltip plugin\n\nabbr[title],\nabbr[data-original-title] { // 4\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 1\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\ndfn {\n font-style: italic; // Add the correct font style in Android 4.3-\n}\n\n// stylelint-disable font-weight-notation\nb,\nstrong {\n font-weight: bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n// stylelint-enable font-weight-notation\n\nsmall {\n font-size: 80%; // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n -webkit-text-decoration-skip: objects; // Remove gaps in links underline in iOS 8+ and Safari 8+.\n\n @include hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href)\n// which have not been made explicitly keyboard-focusable (without tabindex).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n\n @include hover-focus {\n color: inherit;\n text-decoration: none;\n }\n\n &:focus {\n outline: 0;\n }\n}\n\n\n//\n// Code\n//\n\n// stylelint-disable font-family-no-duplicate-names\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace; // Correct the inheritance and scaling of font size in all browsers.\n font-size: 1em; // Correct the odd `em` font sizing in all browsers.\n}\n// stylelint-enable font-family-no-duplicate-names\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n // We have @viewport set which causes scrollbars to overlap content in IE11 and Edge, so\n // we force a non-overlapping, non-auto-hiding scrollbar to counteract.\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg:not(:root) {\n overflow: hidden; // Hide the overflow in IE\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $text-muted;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `