Merge pull request #147 from ilatypov/master

Tidy up CSRF lessons.
This commit is contained in:
mayhew64 2015-11-24 19:44:24 -05:00
commit 511ed91130
7 changed files with 378 additions and 248 deletions

View File

@ -626,18 +626,37 @@ public abstract class AbstractLesson extends Screen implements Comparable<Object
/** /**
* Get the link that can be used to request this screen. * Get the link that can be used to request this screen.
* *
* Rendering the link in the browser may result in Javascript sending
* additional requests to perform necessary actions or to obtain data
* relevant to the lesson or the element of the lesson selected by the
* user. Thanks to using the hash mark "#" and Javascript handling the
* clicks, the user will experience less waiting as the pages do not have
* to reload entirely.
*
* @return a {@link java.lang.String} object. * @return a {@link java.lang.String} object.
*/ */
public String getLink() { public String getLink() {
StringBuffer link = new StringBuffer(); StringBuffer link = new StringBuffer(getPath());
// mvc update: // mvc update:
link.append(getPath()).append("/"); return link
link.append(getScreenId()); .append("/").append(getScreenId())
link.append("/"); .append("/").append(getCategory().getRanking()).toString();
link.append(getCategory().getRanking()); }
return link.toString(); /**
* Get the link to the target servlet.
*
* Unlike getLink() this method does not require rendering the output of
* the request to the link in order to execute the servlet's method with
* conventional HTTP query parameters.
*/
public String getServletLink() {
StringBuffer link = new StringBuffer("attack");
return link
.append("?Screen=").append(getScreenId())
.append("&menu=").append(getCategory().getRanking()).toString();
} }
/** /**

View File

@ -1,164 +1,195 @@
define(['jquery', define(['jquery',
'underscore', 'underscore',
'libs/backbone', 'libs/backbone',
'goatApp/model/LessonContentModel', 'goatApp/model/LessonContentModel',
'goatApp/view/LessonContentView', 'goatApp/view/LessonContentView',
'goatApp/view/PlanView', 'goatApp/view/PlanView',
'goatApp/view/SourceView', 'goatApp/view/SourceView',
'goatApp/view/SolutionView', 'goatApp/view/SolutionView',
'goatApp/view/HintView', 'goatApp/view/HintView',
'goatApp/view/HelpControlsView', 'goatApp/view/HelpControlsView',
'goatApp/view/CookieView', 'goatApp/view/CookieView',
'goatApp/view/ParamView', 'goatApp/view/ParamView',
'goatApp/model/ParamModel', 'goatApp/model/ParamModel',
'goatApp/support/GoatUtils', 'goatApp/support/GoatUtils',
'goatApp/view/UserAndInfoView', 'goatApp/view/UserAndInfoView',
'goatApp/view/MenuButtonView', 'goatApp/view/MenuButtonView',
'goatApp/model/LessonInfoModel', 'goatApp/model/LessonInfoModel',
'goatApp/view/TitleView' 'goatApp/view/TitleView'
], ],
function($, function($,
_, _,
Backbone, Backbone,
LessonContentModel, LessonContentModel,
LessonContentView, LessonContentView,
PlanView, PlanView,
SourceView, SourceView,
SolutionView, SolutionView,
HintView, HintView,
HelpControlsView, HelpControlsView,
CookieView, CookieView,
ParamView, ParamView,
ParamModel, ParamModel,
GoatUtils, GoatUtils,
UserAndInfoView, UserAndInfoView,
MenuButtonView, MenuButtonView,
LessonInfoModel, LessonInfoModel,
TitleView TitleView
) { ) {
'use strict' 'use strict'
var Controller = function(options) { var Controller = function(options) {
this.lessonContent = new LessonContentModel(); this.lessonContent = new LessonContentModel();
this.lessonView = options.lessonView; this.lessonView = options.lessonView;
_.extend(Controller.prototype,Backbone.Events); _.extend(Controller.prototype,Backbone.Events);
this.start = function() { this.start = function() {
this.listenTo(this.lessonContent,'content:loaded',this.onContentLoaded); this.listenTo(this.lessonContent,'content:loaded',this.onContentLoaded);
this.userAndInfoView = new UserAndInfoView(); this.userAndInfoView = new UserAndInfoView();
this.menuButtonView = new MenuButtonView(); this.menuButtonView = new MenuButtonView();
}; };
this.loadLesson = function(scr,menu,stage) { this.loadLesson = function(scr,menu,stage,num) {
this.titleView = new TitleView(); console.log("Loading a lesson, scr: " + scr + ", menu: " + menu + ", stage: " + stage + ", num: " + num);
this.helpsLoaded = {}; this.titleView = new TitleView();
this.lessonContent.loadData({ this.helpsLoaded = {};
'screen': scr, if (typeof(scr) == "undefined") {
'menu': menu, scr = null;
'stage': stage }
}); if (typeof(menu) == "undefined") {
this.planView = {}; menu = null;
this.solutionView = {}; }
this.sourceView = {}; if (typeof(stage) == "undefined") {
this.lessonHintView = {}; stage = null;
this.screen = scr; }
this.menu = menu; if (typeof(num) == "undefined") {
}; num = null;
}
this.lessonContent.loadData({
'scr': scr,
'menu': menu,
'stage': stage,
'num': num,
});
this.planView = {};
this.solutionView = {};
this.sourceView = {};
this.lessonHintView = {};
this.scr = scr;
this.menu = menu;
this.stage = stage;
this.num = num;
console.log("Lesson loading initiated")
};
this.onInfoLoaded = function() { this.onInfoLoaded = function() {
this.helpControlsView = new HelpControlsView({ console.log("Lesson info loaded")
hasPlan:this.lessonInfoModel.get('hasPlan'), this.helpControlsView = new HelpControlsView({
hasSolution:this.lessonInfoModel.get('hasSolution'), hasPlan:this.lessonInfoModel.get('hasPlan'),
hasSource:this.lessonInfoModel.get('hasSource'), hasSolution:this.lessonInfoModel.get('hasSolution'),
hasHints:(this.lessonInfoModel.get('numberHints') > 0), hasSource:this.lessonInfoModel.get('hasSource'),
}); hasHints:(this.lessonInfoModel.get('numberHints') > 0),
});
this.listenTo(this.helpControlsView,'plan:show',this.hideShowHelps); this.listenTo(this.helpControlsView,'plan:show',this.hideShowHelps);
this.listenTo(this.helpControlsView,'solution:show',this.hideShowHelps); this.listenTo(this.helpControlsView,'solution:show',this.hideShowHelps);
this.listenTo(this.helpControlsView,'hints:show',this.onShowHints) this.listenTo(this.helpControlsView,'hints:show',this.onShowHints)
this.listenTo(this.helpControlsView,'source:show',this.hideShowHelps); this.listenTo(this.helpControlsView,'source:show',this.hideShowHelps);
this.listenTo(this.helpControlsView,'lesson:restart',this.restartLesson); this.listenTo(this.helpControlsView,'lesson:restart',this.restartLesson);
this.helpControlsView.render(); this.helpControlsView.render();
this.titleView.render(this.lessonInfoModel.get('lessonTitle')); this.titleView.render(this.lessonInfoModel.get('lessonTitle'));
}; };
this.onContentLoaded = function(loadHelps) { this.onContentLoaded = function(loadHelps) {
this.lessonInfoModel = new LessonInfoModel(); console.log("Lesson content loaded")
this.listenTo(this.lessonInfoModel,'info:loaded',this.onInfoLoaded); this.lessonInfoModel = new LessonInfoModel();
this.listenTo(this.lessonInfoModel,'info:loaded',this.onInfoLoaded);
if (loadHelps) { if (loadHelps) {
this.helpControlsView = null; this.helpControlsView = null;
this.lessonView.model = this.lessonContent; this.lessonView.model = this.lessonContent;
this.lessonView.render(); this.lessonView.render();
this.planView = new PlanView(); this.planView = new PlanView();
this.solutionView = new SolutionView(); this.solutionView = new SolutionView();
this.sourceView = new SourceView(); this.sourceView = new SourceView();
this.lessonHintView = new HintView(); this.lessonHintView = new HintView();
this.cookieView = new CookieView(); this.cookieView = new CookieView();
//TODO: instantiate model with values (not sure why was not working before) //TODO: instantiate model with values (not sure why was not working before)
var paramModel = new ParamModel({}); var paramModel = new ParamModel({});
paramModel.set('screenParam',this.lessonContent.get('screenParam')); paramModel.set('scrParam',this.lessonContent.get('scrParam'));
paramModel.set('menuParam',this.lessonContent.get('menuParam')); paramModel.set('menuParam',this.lessonContent.get('menuParam'));
paramModel.set('stageParam',this.lessonContent.get('stageParam')); paramModel.set('stageParam',this.lessonContent.get('stageParam'));
this.paramView = new ParamView({model:paramModel}); paramModel.set('numParam',this.lessonContent.get('numParam'));
this.paramView = new ParamView({model:paramModel});
$('.lesson-help').hide(); $('.lesson-help').hide();
} }
this.trigger('menu:reload'); this.trigger('menu:reload');
}; };
this.addCurHelpState = function (curHelp) { this.addCurHelpState = function (curHelp) {
this.helpsLoaded[curHelp.helpElement] = curHelp.value; this.helpsLoaded[curHelp.helpElement] = curHelp.value;
}; };
this.hideShowHelps = function(showHelp) { this.hideShowHelps = function(showHelp) {
var showId = '#lesson-' + showHelp + '-row'; var showId = '#lesson-' + showHelp + '-row';
var contentId = '#lesson-' + showHelp + '-content'; var contentId = '#lesson-' + showHelp + '-content';
$('.lesson-help').not(showId).hide(); $('.lesson-help').not(showId).hide();
if (!showId) { if (!showId) {
return; return;
} }
if ($(showId).is(':visible')) { if ($(showId).is(':visible')) {
$(showId).hide(); $(showId).hide();
return; return;
} else { } else {
//TODO: move individual .html operations into individual help views //TODO: move individual .html operations into individual help views
switch(showHelp) { switch(showHelp) {
case 'plan': case 'plan':
$(contentId).html(this.planView.model.get('content')); $(contentId).html(this.planView.model.get('content'));
break; break;
case 'solution': case 'solution':
$(showId).html(this.solutionView.model.get('content')); $(showId).html(this.solutionView.model.get('content'));
break; break;
case 'source': case 'source':
$(contentId).html('<pre>' + this.sourceView.model.get('content') + '</pre>'); $(contentId).html('<pre>' + this.sourceView.model.get('content') + '</pre>');
break; break;
} }
$(showId).show(); $(showId).show();
GoatUtils.scrollToHelp() GoatUtils.scrollToHelp()
} }
}; };
this.onShowHints = function() { this.onShowHints = function() {
this.lessonHintView.render(); this.lessonHintView.render();
}; };
this.restartLesson = function() { this.restartLesson = function() {
var self=this; var self=this;
$.ajax({ var fragment = "attack/" + self.scr + "/" + self.menu;
url:'service/restartlesson.mvc', console.log("Navigating to " + fragment);
method:'GET' // Avoiding the trigger event - handle - navigate loop by
}).then(function() { // loading the lesson explicitly (after executing the restart
self.loadLesson(self.screen,self.menu); // servlet).
}); goatRouter.navigate(fragment);
}; // Resetting the user's lesson state (assuming a single browser
// and session per user).
$.ajax({
url:'service/restartlesson.mvc',
method:'GET'
}).done(function(text) {
console.log("Received a response from the restart servlet: '" + text + "'");
// Explicitly loading the lesson instead of triggering an
// event in goatRouter.navigate().
self.loadLesson(self.scr,self.menu);
});
};
}; };
return Controller; return Controller;
}); });

View File

@ -1,46 +1,54 @@
define(['jquery', define(['jquery',
'underscore', 'underscore',
'backbone', 'backbone',
'goatApp/model/HTMLContentModel'], 'goatApp/model/HTMLContentModel'],
function($, function($,
_, _,
Backbone, Backbone,
HTMLContentModel){ HTMLContentModel){
return HTMLContentModel.extend({ return HTMLContentModel.extend({
urlRoot:null, urlRoot:null,
defaults: { defaults: {
items:null, items:null,
selectedItem:null selectedItem:null
}, },
initialize: function (options) { initialize: function (options) {
this.screenParam = null; this.scrParam = null;
this.menuParam = null; this.menuParam = null;
this.stageParam = null; this.stageParam = null;
this.baseUrlRoot = 'attack?Screen='; this.numParam = null;
}, this.baseUrlRoot = 'attack';
loadData: function(options) { },
this.urlRoot = this.baseUrlRoot +options.screen + '&menu=' + options.menu + '&stage=' + options.stage; loadData: function(options) {
this.set('menuParam',options.menu); this.urlRoot = this.baseUrlRoot + "?Screen=" + options.scr + '&menu=' + options.menu;
this.set('screenParam',options.screen); if (options.stage != null) {
this.set('stageParam',options.stage) this.urlRoot += '&stage=' + options.stage;
var self=this; }
this.fetch().then(function(data) { if (options.num != null) {
self.setContent(data); this.urlRoot += '&Num=' + options.num;
}); }
}, this.set('menuParam', options.menu);
this.set('scrParam', options.scr);
this.set('stageParam', options.stage)
this.set('numParam', options.num)
var self = this;
this.fetch().done(function(data) {
self.setContent(data);
});
},
setContent: function(content, loadHelps) { setContent: function(content, loadHelps) {
if (typeof loadHelps === 'undefined') { if (typeof loadHelps === 'undefined') {
loadHelps = true; loadHelps = true;
} }
this.set('content',content); this.set('content',content);
this.trigger('content:loaded',this,loadHelps); this.trigger('content:loaded',this,loadHelps);
}, },
fetch: function (options) { fetch: function (options) {
options = options || {}; options = options || {};
return Backbone.Model.prototype.fetch.call(this, _.extend({ dataType: "html"}, options)); return Backbone.Model.prototype.fetch.call(this, _.extend({ dataType: "html"}, options));
} }
}); });
}); });

View File

@ -19,7 +19,9 @@ define(['jquery',
var GoatAppRouter = Backbone.Router.extend({ var GoatAppRouter = Backbone.Router.extend({
routes: { routes: {
'welcome':'welcomeRoute', 'welcome':'welcomeRoute',
'attack/:scr/:menu(/:stage)':'attackRoute', 'attack/:scr/:menu':'attackRoute',
'attack/:scr/:menu/:stage':'attackRoute',
'attack/:scr/:menu/*stage/:num':'attackRoute',
}, },
lessonController: new LessonController({ lessonController: new LessonController({
@ -35,14 +37,17 @@ define(['jquery',
this.lessonController.start(); this.lessonController.start();
// this.menuController.initMenu(); // this.menuController.initMenu();
goatRouter.on('route:attackRoute', function(scr,menu,stage) { goatRouter.on('route:attackRoute', function(scr,menu,stage,num) {
this.lessonController.loadLesson(scr,menu,stage); this.lessonController.loadLesson(scr,menu,stage,num);
this.menuController.updateMenu(scr,menu); this.menuController.updateMenu(scr,menu);
//update menu //update menu
}); });
goatRouter.on('route:welcomeRoute', function() { goatRouter.on('route:welcomeRoute', function() {
this.lessonController.loadWelcome(); this.lessonController.loadWelcome();
}); });
goatRouter.on("route", function(route, params) {
console.log("Got a route event: " + route + ", params: " + params);
});
Backbone.history.start(); Backbone.history.start();
this.listenTo(this.lessonController, 'menu:reload',this.reloadMenu) this.listenTo(this.lessonController, 'menu:reload',this.reloadMenu)
@ -57,4 +62,4 @@ define(['jquery',
return GoatAppRouter; return GoatAppRouter;
}); });

View File

@ -1,59 +1,64 @@
//LessonContentView //LessonContentView
define(['jquery', define(['jquery',
'underscore', 'underscore',
'backbone', 'backbone',
'libs/jquery.form'], 'libs/jquery.form'],
function( function(
$, $,
_, _,
Backbone, Backbone,
JQueryForm) { JQueryForm) {
return Backbone.View.extend({ return Backbone.View.extend({
el:'#lesson-content-wrapper', //TODO << get this fixed up in DOM el:'#lesson-content-wrapper', //TODO << get this fixed up in DOM
initialize: function(options) { initialize: function(options) {
options = options || {}; options = options || {};
}, },
render: function() { render: function() {
this.$el.html(this.model.get('content')); this.$el.html(this.model.get('content'));
this.makeFormsAjax(); this.makeFormsAjax();
this.ajaxifyAttackHref(); this.ajaxifyAttackHref();
$(window).scrollTop(0); //work-around til we get the scroll down sorted out $(window).scrollTop(0); //work-around til we get the scroll down sorted out
}, },
//TODO: reimplement this in custom fashion maybe? //TODO: reimplement this in custom fashion maybe?
makeFormsAjax: function () { makeFormsAjax: function () {
var options = { var options = {
success:this.reLoadView.bind(this), success:this.reLoadView.bind(this),
url:'attack?Screen=' + this.model.get('screenParam') + '&menu=' + this.model.get('menuParam'), url: this.model.urlRoot,
type:'GET' type:'GET'
// $.ajax options can be used here too, for example: // $.ajax options can be used here too, for example:
//timeout: 3000 //timeout: 3000
}; };
//hook forms //TODO: clarify form selectors later //hook forms //TODO: clarify form selectors later
$("form").ajaxForm(options); $("form").ajaxForm(options);
}, },
ajaxifyAttackHref: function() { // rewrite any links with hrefs point to relative attack URLs ajaxifyAttackHref: function() { // rewrite any links with hrefs point to relative attack URLs
var self = this; var self = this;
$.each($('a[href^="attack?"]'),function(i,el) { // The current LessonAdapter#getLink() generates a hash-mark link. It will not match the mask below.
var url = $(el).attr('href'); // Besides, the new MVC code registers an event handler that will reload the lesson according to the route.
$(el).unbind('click').attr('href','#').attr('link',url); $.each($('a[href^="attack?"]'),function(i,el) {
//TODO pull currentMenuId var url = $(el).attr('href');
$(el).click(function() { $(el).unbind('click').attr('href','#').attr('link',url);
event.preventDefault(); //TODO pull currentMenuId
var _url = $(el).attr('link'); $(el).click(function(event) {
$.get(_url, {success:self.reloadView.bind(self)}); event.preventDefault();
}); var _url = $(el).attr('link');
}); console.log("About to GET " + _url);
}, $.get(_url)
.done(self.reLoadView.bind(self))
.fail(function() { alert("failed to GET " + _url); });
});
});
},
reLoadView: function(content) { reLoadView: function(content) {
this.model.setContent(content); this.model.setContent(content);
this.render(); this.render();
} }
}); });
}); });

View File

@ -104,7 +104,7 @@
if (stages != null) if (stages != null)
for (int i = 0; i < stages.length; i++) { for (int i = 0; i < stages.length; i++) {
%> %>
<tr><td class="pviimenudivstage"><%=(rla.isStageComplete(webSession, stages[i]) ? lessonComplete : "")%><a href="<%=lesson.getLink() + "&stage=" + (i + 1)%>">Stage <%=i + 1%>: <%=stages[i]%></a> <tr><td class="pviimenudivstage"><%=(rla.isStageComplete(webSession, stages[i]) ? lessonComplete : "")%><a href="<%=lesson.getLink() + "/" + (i + 1)%>">Stage <%=i + 1%>: <%=stages[i]%></a>
</td></tr> </td></tr>
<% <%
} }

View File

@ -0,0 +1,62 @@
package org.owasp.webgoat.lessons;
import org.apache.ecs.Element;
import org.apache.ecs.ElementContainer;
import org.hamcrest.CoreMatchers;
import org.junit.Test;
import org.owasp.webgoat.session.WebSession;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertThat;
public class AbstractLessonTest {
private AbstractLesson lesson = new AbstractLesson() {
protected Element createContent(WebSession s) {
return new ElementContainer();
}
public Category getCategory() {
return Category.XSS;
}
protected Integer getDefaultRanking() {
return new Integer(5);
}
protected Category getDefaultCategory() {
return Category.INTRODUCTION;
}
protected boolean getDefaultHidden() {
return false;
}
protected List<String> getHints(WebSession s) {
return Arrays.<String>asList();
}
public String getInstructions(WebSession s) {
return "Instructions";
}
public String getTitle() {
return "title";
}
public String getCurrentAction(WebSession s) {
return "an action";
}
public void restartLesson() {
}
public void setCurrentAction(WebSession s, String lessonScreen) {
}
};
@Test
public void testLinks() {
String mvcLink = lesson.getLink();
assertThat(mvcLink, CoreMatchers.startsWith("#attack/"));
assertThat(mvcLink, CoreMatchers.endsWith("/900"));
String srvLink = lesson.getServletLink();
assertThat(srvLink, CoreMatchers.startsWith("attack?Screen="));
assertThat(srvLink, CoreMatchers.endsWith("&menu=900"));
}
}