安全性を向上
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"homework-manager/internal/middleware"
|
"homework-manager/internal/middleware"
|
||||||
"homework-manager/internal/service"
|
"homework-manager/internal/service"
|
||||||
|
"homework-manager/internal/validation"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -264,6 +265,11 @@ func (h *APIHandler) CreateAssignment(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := validation.ValidateAssignmentInput(input.Title, input.Description, input.Subject, input.Priority); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
dueDate, err := parseDateString(input.DueDate)
|
dueDate, err := parseDateString(input.DueDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid due_date format. Use RFC3339 or 2006-01-02T15:04"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid due_date format. Use RFC3339 or 2006-01-02T15:04"})
|
||||||
@@ -386,6 +392,11 @@ func (h *APIHandler) UpdateAssignment(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := validation.ValidateAssignmentInput(input.Title, input.Description, input.Subject, input.Priority); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
title := input.Title
|
title := input.Title
|
||||||
if title == "" {
|
if title == "" {
|
||||||
title = existing.Title
|
title = existing.Title
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"homework-manager/internal/middleware"
|
"homework-manager/internal/middleware"
|
||||||
"homework-manager/internal/models"
|
"homework-manager/internal/models"
|
||||||
"homework-manager/internal/service"
|
"homework-manager/internal/service"
|
||||||
|
"homework-manager/internal/validation"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -126,6 +127,22 @@ func (h *AssignmentHandler) Create(c *gin.Context) {
|
|||||||
priority := c.PostForm("priority")
|
priority := c.PostForm("priority")
|
||||||
dueDateStr := c.PostForm("due_date")
|
dueDateStr := c.PostForm("due_date")
|
||||||
|
|
||||||
|
if err := validation.ValidateAssignmentInput(title, description, subject, priority); err != nil {
|
||||||
|
role, _ := c.Get(middleware.UserRoleKey)
|
||||||
|
name, _ := c.Get(middleware.UserNameKey)
|
||||||
|
RenderHTML(c, http.StatusOK, "assignments/new.html", gin.H{
|
||||||
|
"title": "課題登録",
|
||||||
|
"error": err.Error(),
|
||||||
|
"formTitle": title,
|
||||||
|
"description": description,
|
||||||
|
"subject": subject,
|
||||||
|
"priority": priority,
|
||||||
|
"isAdmin": role == "admin",
|
||||||
|
"userName": name,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
reminderEnabled := c.PostForm("reminder_enabled") == "on"
|
reminderEnabled := c.PostForm("reminder_enabled") == "on"
|
||||||
reminderAtStr := c.PostForm("reminder_at")
|
reminderAtStr := c.PostForm("reminder_at")
|
||||||
var reminderAt *time.Time
|
var reminderAt *time.Time
|
||||||
@@ -298,6 +315,11 @@ func (h *AssignmentHandler) Update(c *gin.Context) {
|
|||||||
priority := c.PostForm("priority")
|
priority := c.PostForm("priority")
|
||||||
dueDateStr := c.PostForm("due_date")
|
dueDateStr := c.PostForm("due_date")
|
||||||
|
|
||||||
|
if err := validation.ValidateAssignmentInput(title, description, subject, priority); err != nil {
|
||||||
|
c.Redirect(http.StatusFound, "/assignments")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
reminderEnabled := c.PostForm("reminder_enabled") == "on"
|
reminderEnabled := c.PostForm("reminder_enabled") == "on"
|
||||||
reminderAtStr := c.PostForm("reminder_at")
|
reminderAtStr := c.PostForm("reminder_at")
|
||||||
var reminderAt *time.Time
|
var reminderAt *time.Time
|
||||||
|
|||||||
173
internal/validation/validation.go
Normal file
173
internal/validation/validation.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
var MaxLengths = map[string]int{
|
||||||
|
"title": 200,
|
||||||
|
"description": 5000,
|
||||||
|
"subject": 100,
|
||||||
|
"priority": 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
var xssPatterns = []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`(?i)<\s*script`),
|
||||||
|
regexp.MustCompile(`(?i)</\s*script`),
|
||||||
|
regexp.MustCompile(`(?i)javascript\s*:`),
|
||||||
|
regexp.MustCompile(`(?i)on\w+\s*=`),
|
||||||
|
regexp.MustCompile(`(?i)<\s*iframe`),
|
||||||
|
regexp.MustCompile(`(?i)<\s*object`),
|
||||||
|
regexp.MustCompile(`(?i)<\s*embed`),
|
||||||
|
regexp.MustCompile(`(?i)<\s*svg[^>]*on\w+\s*=`),
|
||||||
|
regexp.MustCompile(`(?i)data\s*:\s*text/html`),
|
||||||
|
regexp.MustCompile(`(?i)<\s*img[^>]*on\w+\s*=`),
|
||||||
|
regexp.MustCompile(`(?i)expression\s*\(`),
|
||||||
|
regexp.MustCompile(`(?i)alert\s*\(`),
|
||||||
|
regexp.MustCompile(`(?i)confirm\s*\(`),
|
||||||
|
regexp.MustCompile(`(?i)prompt\s*\(`),
|
||||||
|
regexp.MustCompile(`(?i)document\s*\.\s*cookie`),
|
||||||
|
regexp.MustCompile(`(?i)document\s*\.\s*location`),
|
||||||
|
regexp.MustCompile(`(?i)window\s*\.\s*location`),
|
||||||
|
}
|
||||||
|
|
||||||
|
var sqlInjectionPatterns = []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`(?i)'\s*or\s+`),
|
||||||
|
regexp.MustCompile(`(?i)'\s*and\s+`),
|
||||||
|
regexp.MustCompile(`(?i)"\s*or\s+`),
|
||||||
|
regexp.MustCompile(`(?i)"\s*and\s+`),
|
||||||
|
regexp.MustCompile(`(?i)union\s+(all\s+)?select`),
|
||||||
|
regexp.MustCompile(`(?i);\s*(drop|delete|update|insert|alter|truncate)\s+`),
|
||||||
|
regexp.MustCompile(`(?i)--\s*$`),
|
||||||
|
regexp.MustCompile(`(?i)/\*.*\*/`),
|
||||||
|
regexp.MustCompile(`(?i)'\s*;\s*`),
|
||||||
|
regexp.MustCompile(`(?i)exec\s*\(`),
|
||||||
|
regexp.MustCompile(`(?i)xp_\w+`),
|
||||||
|
regexp.MustCompile(`(?i)load_file\s*\(`),
|
||||||
|
regexp.MustCompile(`(?i)into\s+(out|dump)file`),
|
||||||
|
regexp.MustCompile(`(?i)benchmark\s*\(`),
|
||||||
|
regexp.MustCompile(`(?i)sleep\s*\(\s*\d`),
|
||||||
|
regexp.MustCompile(`(?i)waitfor\s+delay`),
|
||||||
|
regexp.MustCompile(`(?i)1\s*=\s*1`),
|
||||||
|
regexp.MustCompile(`(?i)'1'\s*=\s*'1`),
|
||||||
|
}
|
||||||
|
|
||||||
|
var pathTraversalPatterns = []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`\.\.[\\/]`),
|
||||||
|
regexp.MustCompile(`\.\.%2[fF]`),
|
||||||
|
regexp.MustCompile(`%2e%2e[\\/]`),
|
||||||
|
regexp.MustCompile(`\.\./`),
|
||||||
|
regexp.MustCompile(`\.\.\\`),
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandInjectionPatterns = []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`^\s*;`),
|
||||||
|
regexp.MustCompile(`;\s*\w+`),
|
||||||
|
regexp.MustCompile(`\|\s*\w+`),
|
||||||
|
regexp.MustCompile("`[^`]+`"),
|
||||||
|
regexp.MustCompile(`\$\([^)]+\)`),
|
||||||
|
regexp.MustCompile(`&&\s*\w+`),
|
||||||
|
regexp.MustCompile(`\|\|\s*\w+`),
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValidationError struct {
|
||||||
|
Field string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ValidationError) Error() string {
|
||||||
|
return fmt.Sprintf("%s: %s", e.Field, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateAssignmentInput(title, description, subject, priority string) error {
|
||||||
|
if err := ValidateField("title", title, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ValidateField("description", description, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ValidateField("subject", subject, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ValidateField("priority", priority, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateField(fieldName, value string, required bool) error {
|
||||||
|
if required && strings.TrimSpace(value) == "" {
|
||||||
|
return &ValidationError{Field: fieldName, Message: "必須項目です"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxLen, ok := MaxLengths[fieldName]; ok {
|
||||||
|
if len(value) > maxLen {
|
||||||
|
return &ValidationError{
|
||||||
|
Field: fieldName,
|
||||||
|
Message: fmt.Sprintf("最大%d文字までです", maxLen),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldName != "description" {
|
||||||
|
for _, r := range value {
|
||||||
|
if unicode.IsControl(r) && r != '\n' && r != '\r' && r != '\t' {
|
||||||
|
return &ValidationError{
|
||||||
|
Field: fieldName,
|
||||||
|
Message: "不正な制御文字が含まれています",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pattern := range xssPatterns {
|
||||||
|
if pattern.MatchString(value) {
|
||||||
|
return &ValidationError{
|
||||||
|
Field: fieldName,
|
||||||
|
Message: "潜在的に危険なHTMLタグまたはスクリプトが含まれています",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pattern := range sqlInjectionPatterns {
|
||||||
|
if pattern.MatchString(value) {
|
||||||
|
return &ValidationError{
|
||||||
|
Field: fieldName,
|
||||||
|
Message: "潜在的に危険なSQL構文が含まれています",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pattern := range pathTraversalPatterns {
|
||||||
|
if pattern.MatchString(value) {
|
||||||
|
return &ValidationError{
|
||||||
|
Field: fieldName,
|
||||||
|
Message: "不正なパス文字列が含まれています",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pattern := range commandInjectionPatterns {
|
||||||
|
if pattern.MatchString(value) {
|
||||||
|
return &ValidationError{
|
||||||
|
Field: fieldName,
|
||||||
|
Message: "潜在的に危険なコマンド構文が含まれています",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SanitizeString(s string) string {
|
||||||
|
s = strings.ReplaceAll(s, "\x00", "")
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -1,28 +1,56 @@
|
|||||||
// Homework Manager JavaScript
|
const XSS = {
|
||||||
|
escapeHtml: function (str) {
|
||||||
|
if (str === null || str === undefined) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = str;
|
||||||
|
return div.innerHTML;
|
||||||
|
},
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
setTextSafe: function (element, text) {
|
||||||
// Auto-dismiss alerts after 5 seconds (exclude alerts inside modals)
|
if (element) {
|
||||||
|
element.textContent = text;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
sanitizeUrl: function (url) {
|
||||||
|
if (!url) return '';
|
||||||
|
const cleaned = String(url).replace(/[\x00-\x1F\x7F]/g, '').trim();
|
||||||
|
try {
|
||||||
|
const parsed = new URL(cleaned, window.location.origin);
|
||||||
|
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
|
||||||
|
return parsed.href;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (cleaned.startsWith('/') && !cleaned.startsWith('//')) {
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.XSS = XSS;
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const alerts = document.querySelectorAll('.alert:not(.alert-danger):not(.modal .alert)');
|
const alerts = document.querySelectorAll('.alert:not(.alert-danger):not(.modal .alert)');
|
||||||
alerts.forEach(function(alert) {
|
alerts.forEach(function (alert) {
|
||||||
setTimeout(function() {
|
setTimeout(function () {
|
||||||
alert.classList.add('fade');
|
alert.classList.add('fade');
|
||||||
setTimeout(function() {
|
setTimeout(function () {
|
||||||
alert.remove();
|
alert.remove();
|
||||||
}, 150);
|
}, 150);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Confirm dialogs for dangerous actions
|
|
||||||
const confirmForms = document.querySelectorAll('form[data-confirm]');
|
const confirmForms = document.querySelectorAll('form[data-confirm]');
|
||||||
confirmForms.forEach(function(form) {
|
confirmForms.forEach(function (form) {
|
||||||
form.addEventListener('submit', function(e) {
|
form.addEventListener('submit', function (e) {
|
||||||
if (!confirm(form.dataset.confirm)) {
|
if (!confirm(form.dataset.confirm)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set default datetime to now + 1 day for new assignments
|
|
||||||
const dueDateInput = document.getElementById('due_date');
|
const dueDateInput = document.getElementById('due_date');
|
||||||
if (dueDateInput && !dueDateInput.value) {
|
if (dueDateInput && !dueDateInput.value) {
|
||||||
const tomorrow = new Date();
|
const tomorrow = new Date();
|
||||||
|
|||||||
Reference in New Issue
Block a user