UNPKG

sitecheck

Version:

Open Source web application security scanner

469 lines (415 loc) 18.9 kB
/** * @license Apache-2.0 * Copyright (C) 2016 The Sitecheck Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 'use strict'; var http = require('http'); var qs = require('querystring'); //var tough = require('tough-cookie'); var Promise = require('bluebird'); var helpers = require('../../../src/helpers.js'); var Target = require('../../../src/target.js'); var cancellationToken = require('../../../src/cancellationToken.js'); var AutoLogin = require('../../../src/autoLogin.js'); var request = require('../../../src/requestwrapper.js'); var SessionHelper = require('../../helpers/sessionHelper.js'); var CheckCsrf = require('../../../src/checks/page/check_csrf.js'); const CONSTANTS = require('../../../src/constants.js'); var params = require('../../../src/params.js'); var inputVector = require('../../../src/inputVector.js'); var autoLogin = new AutoLogin(); var sessionHelper = new SessionHelper(); var ct = new cancellationToken(); var fields = { action: '/connect', username: 'bob', password: 'passwd', csrf: 'authenticity_token', csrf_value: helpers.token() }; var server = http.createServer(function (req, res) { let csrfToken = sessionHelper.getCsrfToken(req, res); if (req.url == '/login') { sessionHelper.manageSession(req, res); res.writeHead(200, { "Content-Type": "text/html" }); res.end('<form action="/connect" method="POST"><input type="text" name="username"/><input type="password" name="password"/>' + '<input type="submit" value="submit"/></form>'); } else if (req.url == '/connect') { sessionHelper.manageSession(req, res); // user must have a valid existing sessid to connect if (!sessionHelper.isValidSession(req)) { res.writeHead(403); res.end('bad request : invalid sessid'); return; } var body = ''; req.on('data', function (data) { body += data; // Prevent malicious flooding if (body.length > 1e6) req.connection.destroy(); }); req.on('end', function () { var post = qs.parse(body); if (post.password == fields.password && post.username == fields.username) { res.writeHead(302, { 'Location': '/content' }); sessionHelper.connectSession(req); res.end(); } else { res.writeHead(403); res.end('bad request : wrong credentials'); } }); } else if (req.url == '/empty_body') { res.writeHead(200, { "Content-Type": "text/html" }); res.end(''); } else if (req.url == '/empty_body_2') { res.writeHead(200, { "Content-Type": "text/html" }); res.end(); } else if (req.url == '/content') { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('<html><head><body>content<body></head></html>'); } else if (req.url == '/csrf_ok1') { // A page only accessible in connected mode if (sessionHelper.isValidSession(req)) { res.writeHead(200, { "Content-Type": "text/html" }); res.end('<html><body>' + '<form action="/abcd">' + '<input type="text" name="comment"/>' + '<input type="submit" value="submit"/><input type="hidden" name="' + fields.csrf + '" value="' + csrfToken + '"/> ' + '</form>' + '</body></html>'); } else { res.writeHead(403, { "Content-Type": "text/plain" }); res.end('restricted access'); } } else if (req.url == '/csrf_ok2') { // A page with 1 or 2 forms. The second one only appears when connected // Form enctype is "multipart/form-data" if (sessionHelper.isValidSession(req)) { res.writeHead(200, { "Content-Type": "text/html" }); res.end('<html><body>' + '<form action="/abcd" enctype="multipart/form-data">' + '<input type="text" name="comment"/>' + '<input type="submit" value="submit"/><input type="hidden" name="' + fields.csrf + '" value="' + csrfToken + '"/> ' + '</form>' + '<form><input type="text" name="comment2"/><input type="submit2" value="submit"/></form>' + '</body></html>'); } else { res.writeHead(200, { "Content-Type": "text/html" }); res.end('<html><body>' + '<form><input type="text" name="comment2"/><input type="submit2" value="submit"/></form>' + '</body></html> '); } } else if (req.url == '/no_token') { // A form only accessible in connected mode, with no csrf token if (sessionHelper.isValidSession(req)) { res.writeHead(200, { "Content-Type": "text/html" }); res.end('<html><body>' + '<form action="/abcd">' + '<input type="text" name="comment"/>' + '<input type="submit" value="submit"/><input type="hidden" name="blabla" value="blabla"/> ' + '</form>' + '</body></html>'); } else { res.writeHead(403, { "Content-Type": "text/plain" }); res.end('restricted access'); } } else if (req.url == '/constantToken') { // A form only accessible in connected mode, with a constant token var constantCsrfToken = "ojiod5ef894e9:dzsfzf5f4"; if (sessionHelper.isValidSession(req)) { res.writeHead(200, { "Content-Type": "text/html" }); res.end('<html><body>' + '<form action="/abcd" enctype="multipart/form-data">' + '<input type="text" name="comment"/>' + '<input type="submit" value="submit"/><input type="hidden" name="blabla" value="blabla"/> ' + '<input type="hidden" name="csrf" value="' + constantCsrfToken + '"/> ' + '</form>' + '</body></html>'); } else { res.writeHead(403, { "Content-Type": "text/plain" }); res.end('restricted access'); } } else if (req.url == '/uncheckedToken') { // A page only accessible in connected mode. Form's action url does not check token validity if (sessionHelper.isValidSession(req)) { res.writeHead(200, { "Content-Type": "text/html" }); res.end('<html><body>' + '<form action="/actionpage">' + '<input type="text" name="comment"/>' + '<input type="submit" value="submit"/><input type="hidden" name="' + fields.csrf + '" value="' + csrfToken + '"/> ' + '</form>' + '</body></html>'); } else { res.writeHead(403, { "Content-Type": "text/plain" }); res.end('restricted access'); } } else if (req.url == '/actionpage') { // An action page that does not check csrf token res.writeHead(302, { "Content-Type": "text/html" }); res.end('<html><body>' + '<div>hello</div>' + '</body></html>'); } else if (req.url == '/formless') { // A page with no form if (sessionHelper.isValidSession(req)) { res.writeHead(200, { "Content-Type": "text/html" }); res.end('<html><body>' + '<div>hello</div>' + '</body></html>'); } else { res.writeHead(403, { "Content-Type": "text/plain" }); res.end('restricted access'); } } else if (req.url == '/falsepositive') { // A well known false positive ajax form if (sessionHelper.isValidSession(req)) { res.writeHead(200, { "Content-Type": "text/html" }); res.end('<html><body>' + '<form action="/abcd">' + '<input type="text" name="stripe-card-number"/>' + // Stripe-like form '<input type="text" name="comment"/>' + '<input type="submit" value="submit"/><input type="hidden" name="blabla" value="blabla"/> ' + '</form>' + '</body></html>'); } else { res.writeHead(403, { "Content-Type": "text/plain" }); res.end('restricted access'); } } else if (req.url == '/unreachableaction') { // A form with an unreachable action url if (sessionHelper.isValidSession(req)) { res.writeHead(200, { "Content-Type": "text/html" }); res.end('<html><body>' + '<form action="http://zz9e79ge7t9g78e89eg486erg86erg8.com">' + '<input type="text" name="comment"/>' + '<input type="hidden" name="' + fields.csrf + '" value="' + csrfToken + '"/>' + '<input type="submit" value="submit"/><input type="hidden" name="blabla" value="blabla"/> ' + '</form>' + '</body></html>'); } else { res.writeHead(403, { "Content-Type": "text/plain" }); res.end('restricted access'); } } else { res.writeHead(404); res.end('wrong request'); } }); describe('checks/page/check_csrf.js', function () { this.timeout(50000); before((done) => { server.listen(8000); params.loginPage = 'http://localhost:8000/login'; params.user = fields.username; params.password = fields.password; // get a connected session autoLogin.login(params.loginPage, params.user, params.password, ct, (err, data) => { if (err) { done(new Error("login failed.")); } else { if (!data) done(new Error("No data")); if (!data.cookieJar) done(new Error("No data.cookieJar")); // we're logged in, preserve cookies for all subsequent requests request.sessionJar = data.cookieJar; done(); } }); }); it('passes csrf protected forms', (done) => { var urls = ['http://localhost:8000/csrf_ok1', 'http://localhost:8000/csrf_ok2']; Promise.each(urls, (item, index, length) => { let target = new Target(item, CONSTANTS.TARGETTYPE.PAGE); let check = new CheckCsrf(target); return check.check(ct); }).then((value) => { done(); }).catch((value) => { done(new Error("Unexpected error")); }); }); it('detects unprotected forms', (done) => { let target = new Target('http://localhost:8000/no_token', CONSTANTS.TARGETTYPE.PAGE); let check = new CheckCsrf(target); check.check(ct).then((value) => { done(new Error("Expected issue not raised")); }).catch((value) => { done(); }); }); it('detects unchecked cross session tokens', (done) => { let target = new Target('http://localhost:8000/uncheckedToken', CONSTANTS.TARGETTYPE.PAGE); let check = new CheckCsrf(target); check.check(ct).then((value) => { done(new Error("Expected issue not raised")); }).catch((value) => { done(); }); }); it('detects constant csrf tokens', (done) => { let target = new Target('http://localhost:8000/constantToken', CONSTANTS.TARGETTYPE.PAGE); let check = new CheckCsrf(target); check.check(ct).then((value) => { done(new Error("Expected issue not raised")); }).catch((value) => { if (value && value.length && value[0].errorContent.indexOf("same accross") !== -1) { done(); } else { done(new Error("Unexpected error")); } }); }); it('passes form-less pages', (done) => { let target = new Target('http://localhost:8000/formless', CONSTANTS.TARGETTYPE.PAGE); let check = new CheckCsrf(target); check.check(ct).then((value) => { done(); }).catch((value) => { done(new Error("Unexpected issue raised")); }); }); it('passes unreachable pages', (done) => { let target = new Target('http://localhost:8001/unreachable', CONSTANTS.TARGETTYPE.PAGE); let check = new CheckCsrf(target); check.check(ct).then((value) => { done(); }).catch((value) => { done(new Error("Unexpected issue raised")); }); }); it('passes false positives', (done) => { let target = new Target('http://localhost:8000/falsepositive', CONSTANTS.TARGETTYPE.PAGE); let check = new CheckCsrf(target); check.check(ct).then((value) => { done(); }).catch((value) => { done(new Error("Unexpected issue raised")); }); }); it('passes a form with an unreachable action url', (done) => { // check_csrf must remain silent on this type of issue. It's SEO checks responsibility to raise this kind of issue. let target = new Target('http://localhost:8000/unreachableaction', CONSTANTS.TARGETTYPE.PAGE); let check = new CheckCsrf(target); check.check(ct).then((value) => { done(); }).catch((value) => { done(new Error("Unexpected issue raised")); }); }); it('gets an error when getting another Token', (done) => { let target = new Target('http://localhost:8000/unreachableaction', CONSTANTS.TARGETTYPE.PAGE); let check = new CheckCsrf(target); let ct = new cancellationToken(); check.getAnotherToken(ct, (err, data) => { if (err) { done(); } else { done(new Error("Expected error not raised")); } }); ct.cancel(); }); it('tries to access an inexistant URL', (done) => { let target = new Target('http://localhost:8000/not_a_login_url', CONSTANTS.TARGETTYPE.PAGE); let check = new CheckCsrf(target); check.getAnotherToken(ct, (err, data) => { if (err) { done(new Error("Expected error not raised")); } else { done(); } }); }); it('is called with empty body', (done) => { let target = new Target('http://localhost:8000/empty_body', CONSTANTS.TARGETTYPE.PAGE); let check = new CheckCsrf(target); check.getAnotherToken(ct, (err, data) => { if (err) { done(new Error("Expected error not raised")); } else { done(); } }); }); it('is called with empty inputVector', (done) => { let target = new Target('http://localhost:8000/empty_body', CONSTANTS.TARGETTYPE.PAGE); let check = new CheckCsrf(target); check.getCsrfField(null); done(); }); it('is called with no field in inputVector', (done) => { let target = new Target('http://localhost:8000/empty_body', CONSTANTS.TARGETTYPE.PAGE); let check = new CheckCsrf(target); check.getCsrfField({fields: ''}); done(); }); it('is does not have an obvious token name', (done) => { var iv = new inputVector.InputVector('http://localhost:8000/no_token', 'not_obvious', 'get', [{type: 'hidden', name: 'token_not_obvious'}, {type: 'hidden', name: 'token_not_obvious'}], null); let target = new Target('http://localhost:8000/no_token', CONSTANTS.TARGETTYPE.PAGE); let check = new CheckCsrf(target); check.getCsrfField(iv); done(); }); it.skip('hacks Concrete5 < v5.7.3.2', (done) => { params.loginPage = 'https://progressive-sports.co.uk/login'; params.user = 'sitecheck.ut@gmail.com'; params.password = 'sitechec'; // get a connected session autoLogin.login(params.loginPage, params.user, params.password, ct, (err, data) => { if (err) { done(new Error("login failed.")); } else { if (!data) done(new Error("No data")); if (!data.cookieJar) done(new Error("No data.cookieJar")); // we're logged in, preserve cookies for all subsequent requests request.sessionJar = data.cookieJar; let target = new Target('https://progressive-sports.co.uk/profile/edit/', CONSTANTS.TARGETTYPE.PAGE); let check = new CheckCsrf(target); check.check(ct).then((value) => { done(new Error("Expected issue not raised")); }).catch((value) => { done(); }); } }); }); it.skip(' is able to check a woocommerce.com form is correctly protected', (done) => { params.loginPage = 'https://woocommerce.com/my-account/'; params.user = 'sitecheck.ut@gmail.com'; params.password = 'sitechec'; // get a connected session autoLogin.login(params.loginPage, params.user, params.password, ct, (err, data) => { if (err) { done(err); } else { if (!data) done(new Error("No data")); if (!data.cookieJar) done(new Error("No data.cookieJar")); // we're logged in, preserve cookies for all subsequent requests request.sessionJar = data.cookieJar; let target = new Target('https://woocommerce.com/my-account/', CONSTANTS.TARGETTYPE.PAGE); let check = new CheckCsrf(target); check.check(ct).then((value) => { done(); }).catch((value) => { done(new Error("Unexpected issue raised")); }); } }); }); after(() => { server.close(); }); });