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

Manage Submissions #2248

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions app/assets/javascripts/annotations_helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/* following paths/functions for annotations */
var sharedCommentsPath = basePath + "/shared_comments";

var createPath = basePath + ".json";
var updatePath = function (ann) {
return [basePath, "/", ann.id, ".json"].join("");
};
var deletePath = updatePath;

function getSharedCommentsForProblem(problem_id) {
return localCache['shared_comments'][problem_id]?.map(
(annotation) => {
return {label: annotation.comment ?? annotation, value: annotation}
}
)
}

var selectAnnotation = box => (e, ui) => {
const {value} = ui.item;

const score = value.value ?? 0;
box.find('#comment-score').val(score);

const $textarea = box.find("#comment-textarea");
M.textareaAutoResize($textarea);

return false;
}

function focusAnnotation( event, ui ) {
$(this).val(ui.item.label);
return false;
}

// retrieve shared comments
// also retrieves annotation id to allow easy deletion in the future
function retrieveSharedComments(cb) {
$.getJSON(sharedCommentsPath, function (data) {
localCache['shared_comments'] = {};
data.forEach(e => {
if (!e.problem_id)
return;
localCache['shared_comments'][e.problem_id] ||= [];
localCache['shared_comments'][e.problem_id].push(e);
});
cb?.();
});
}

function purgeCurrentPageCache() {
localCache[currentHeaderPos] = {
codeBox: `<div id="code-box">${$('#code-box').html()}</div>`,
pdf: false,
symbolTree: `<div id="symbol-tree-box">${$('#symbol-tree-box').html()}</div>`,
versionLinks: `<span id="version-links">${$('#version-links').html()}</span>`,
versionDropdown: `<span id="version-dropdown">${$('#version-dropdown').html()}</span>`,
url: window.location.href,
};
}

function plusFix(n) {
n = parseFloat(n)
if (isNaN(n)) n = 0;

if (n > 0) {
return "+" + n.toFixed(2);
}

return n.toFixed(2);
}

function getProblemNameWithId(problem_id) {
var problem_id = parseInt(problem_id, 10);
var problem = _.findWhere(problems, { "id": problem_id });
if (problem == undefined) return "Deleted Problem(s)";
return problem.name;
}


// create an HTML element real nice and easy like
function elt(t, a) {
var el = document.createElement(t);
if (a) {
for (var attr in a)
if (a.hasOwnProperty(attr))
el.setAttribute(attr, a[attr]);
}
for (var i = 2; i < arguments.length; ++i) {
var arg = arguments[i];
if (typeof arg === "string")
arg = document.createTextNode(arg);
el.appendChild(arg);
}
return el;
}


// this creates a JSON representation of what the actual Rails Annotation model looks like
function createAnnotation() {
var annObj = {
submitted_by: cudEmailStr,
};
if (fileNameStr != null) {
annObj.filename = fileNameStr
}

if (currentHeaderPos || currentHeaderPos === 0) {
annObj.position = currentHeaderPos
}

return annObj;
}

function getAnnotationObject(annotationId) {
for (var i = 0; i < annotations.length; i++) {
if (annotations[i].id == annotationId) {
return annotations[i];
}
}
}


var updateAnnotationBox = function (annObj) {

var problemStr = annObj.problem_id ? getProblemNameWithId(annObj.problem_id) : "General";
var valueStr = annObj.value ? annObj.value.toString() : "None";
var commentStr = annObj.comment;

if (annotationMode === "PDF") {
$('#ann-box-' + annObj.id).find('.score-box').html("<div>Problem: " + problemStr + "</div><div> Score: " + valueStr + "</div>");
$("#ann-box-" + annObj.id).find('.body').html(commentStr);
}
else {
$('#ann-box-' + annObj.id).find('.score-box').html("<span>" + problemStr + "</span><span>" + valueStr + "</span>");
}
$('#ann-box-' + annObj.id).find('.edit').show();
$('#ann-box-' + annObj.id).find('.body').show();
$('#ann-box-' + annObj.id).find('.score-box').show();
$('#ann-box-' + annObj.id).find('.minimize').show();
$('#ann-box-' + annObj.id).draggable('enable');
$('#ann-box-' + annObj.id).resizable('enable');
}
158 changes: 158 additions & 0 deletions app/assets/javascripts/annotations_popup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
const updateEditTweakButtons = () => {
tweaks.forEach(({tweak, submission}) => {
get_tweak_total(submission.id).then(data => {
tweak?.setState({ amount: data })
})
})
}
const get_tweak_total = (submission_id) => {
return new Promise((resolve, reject) => {
$.ajax({
url: `submissions/${submission_id}/tweak_total`,
method: 'GET',
dataType: 'json',
success: (data) => {
resolve(data);
},
error: (error) => {
console.error("There was an error fetching the scores:", error);
reject(error);
}
});
});
}
function newAnnotationFormCode() {
var box = $(".base-annotation-line").clone();
box.removeClass("base-annotation-line");

box.addClass("new-annotation");

// Creates a dictionary of problem and grader_id
var problemGraderId = {};

_.each(scores, function (score) {
problemGraderId[score.problem_id] = score.grader_id;
});

_.each(problems, function (problem, i) {
if (problemGraderId[problem.id] !== 0) { // Because grader == 0 is autograder
box.find("select")?.append(
$("<option />").val(problem.id).text(problem.name)
);
}
});

box.find('.annotation-form').show();
box.find('.annotation-cancel-button').click(function (e) {
e.preventDefault();
$(this).parents(".annotation-form").parent().remove();
$('#annotation-modal').modal('close');
})

box.find('#comment-textarea').autocomplete({
appendTo: box.find('#comment-textarea').parent(),
source: getSharedCommentsForProblem(box.find("select").val()) || [],
minLength: 0,
delay: 0,
select: selectAnnotation(box),
focus: focusAnnotation
}).focus(function () {
M.textareaAutoResize($(this));
$(this).autocomplete('search', $(this).val())
});

box.tooltip();

box.find("select").on('change', function () {
const problem_id = $(this).val();

// Update autocomplete to display shared comments for selected problem
box.find("#comment-textarea").autocomplete({
source: getSharedCommentsForProblem(problem_id) || []
});
});

box.find('.annotation-form').submit(function (e) {
e.preventDefault();
var comment = $(this).find(".comment").val();
var shared_comment = $(this).find("#shared-comment").is(":checked");
var score = $(this).find(".score").val();
var problem_id = $(this).find(".problem-id").val();

if (comment === undefined || comment === "") {
box.find('.error').text("Annotation comment can not be blank!").show();
return;
}

if (score === undefined || score === "") {
box.find('.error').text("Annotation score can not be blank!").show();
return;
}

if (problem_id == undefined) {
if ($('.select').children('option').length > 0)
box.find('.error').text("Problem not selected").show();
else
box.find('.error').text("There are no non-autograded problems. Create a new one at Edit Assessment > Problems").show();
return;
}
submitNewAnnotation(comment, shared_comment, true, score, problem_id, 0, $(this));
});

return box;
}

/* sets up and calls $.ajax to submit an annotation */
var submitNewAnnotation = function (comment, shared_comment, global_comment, value, problem_id, lineInd, form) {
var newAnnotation = createAnnotation();
Object.assign(newAnnotation, { line: parseInt(lineInd), comment, value, problem_id, filename: fileNameStr, shared_comment, global_comment });

if (comment === undefined || comment === "") {
$(form).find('.error').text("Could not save annotation. Please refresh the page and try again.").show();
return;
}

$(form).find('.error').hide();

$.ajax({
url: createPath,
accepts: "json",
dataType: "json",
data: {
annotation: newAnnotation
},
type: "POST",
success: function (data, type) {
updateEditTweakButtons();
$(form).parent().remove();
$('#annotation-modal').modal('close');
},
error: function (result, type) {
$(form).find('.error').text("Could not save annotation. Please refresh the page and try again.").show();
},
complete: function (result, type) { }
});

}

var updateAnnotation = function (annotationObj, box) {
$(box).find(".error").hide();
$.ajax({
url: updatePath(annotationObj),
accepts: "json",
dataType: "json",
data: {
annotation: annotationObj
},
type: "PUT",
success: function (data, type) {
$(box).remove();
displayAnnotations();
},
error: function (result, type) {
$(box).find('.error').text("Failed to save changes to the annotation. Please refresh the page and try again.").show();
},
complete: function (result, type) { }
});
}

45 changes: 45 additions & 0 deletions app/assets/javascripts/autolab_component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Usage:
* // Create a new instance, associating it with the element specified by id 'app'
const MyComponent = new AutolabComponent('app');

// Define a template for the instance
MyComponent.template = function() {
return `
<div>
<p>Name: ${this.state.name}</p>
<p>Age: ${this.state.age}</p>
</div>
`;
};

// Set the initial state
MyComponent.setState({
name: 'John',
age: 30
});

// Later in the code, you can update the state like this:
// MyComponent.setState({ age: 31 });
*/


function AutolabComponent(elementId, initialState = {}, template = () => {}) {
this.elementId = elementId;
this.state = initialState;
this.template = template;

this.setState = function(newState = {}) {
$.extend(this.state, newState);
this.render();
};

this.template = function() {
// Default template; should be overridden by users of the library
return `<div></div>`;
};

this.render = function() {
$(`#${this.elementId}`).html(this.template());
};
Comment on lines +42 to +44
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Based on the analysis of the code and search results, I can now provide the final response:

The template rendering is vulnerable to XSS attacks - HTML escaping is required

The AutolabComponent class uses jQuery's html() method to directly inject template output into the DOM without any sanitization. While there is an escapeHtml utility function available in watchlist.js, it's not being used in the component's rendering logic. This creates a potential XSS vulnerability when user-provided data from this.state is interpolated into templates.

  • Add HTML escaping in the template function before interpolating state values:
this.template = function() {
  return `<div>
    <p>Name: ${escapeHtml(this.state.name)}</p>
    <p>Age: ${escapeHtml(this.state.age)}</p>
  </div>`;
};
  • Import and use the existing escapeHtml utility function from watchlist.js or implement HTML escaping directly in the component.
🔗 Analysis chain

Verify potential XSS vectors when rendering the template.

Because the entire string is injected via jQuery html(), ensure that any user-provided data passed into this.state is sanitized to prevent cross-site scripting attacks.

Generate the following script to confirm that user-provided data is sanitized before being assigned to this.state:


Let me gather more information about the template rendering and potential XSS vectors.


Let me check one more thing to see how the state data is actually used in templates.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Searches for calls to setState that might be constructed from user input.
rg -A 5 "setState\s*\(\s*\{.*" 

Length of output: 1263


Script:

#!/bin/bash
# Check how the template is used and if there's any sanitization
rg -A 5 "template\s*=.*" 

# Check for any sanitization functions or libraries
rg -l "(sanitize|escape|xss)"

# Look for the actual template implementations
ast-grep --pattern 'template = function() {
  $$$
}'

Length of output: 9040


Script:

#!/bin/bash
# Look for template implementations that use state data
rg -A 10 "this\.state\." app/assets/javascripts/

# Check for any HTML escaping utilities
rg -A 5 "escape" app/assets/javascripts/

Length of output: 45506

}
Loading
Loading