别着急,坐和放宽
使用社交账号登录
点击劫持是一种视觉欺骗攻击,攻击者使用透明的iframe覆盖在一个看似正常的网页上,诱导用户点击实际上是隐藏iframe中的恶意内容。
X-Frame-Options选项:
DENY: 完全禁止被嵌入iframeSAMEORIGIN: 只允许同源页面嵌入更现代和灵活的方法:
注意: JavaScript防御可能被绕过,应配合服务器端头部使用。
防护清单:
不安全的文件上传可能导致:
安全上传清单:
SSRF (Server-Side Request Forgery) 是一种安全漏洞,攻击者诱导服务器向攻击者指定的目标发起请求,从而访问内部资源或执行未授权操作。
SSRF防护清单:
序列化: 将对象转换为字节流以便存储或传输 反序列化: 将字节流还原为对象
当应用程序接受来自不可信源的序列化数据并进行反序列化时,如果没有适当验证,可能导致远程代码执行。
防护清单:
本文覆盖了四种重要的高级Web安全威胁及其防护策略:
核心防护要点:
| 攻击类型 | 关键防护措施 |
|---|---|
| 点击劫持 | X-Frame-Options、CSP frame-ancestors |
| 文件上传漏洞 | 类型验证、大小限制、安全存储、病毒扫描 |
| SSRF攻击 | URL白名单、IP黑名单、禁用危险协议 |
| 不安全反序列化 | JSON优先、签名验证、输入验证 |
通用安全原则:
<!-- 攻击者网站 evil.com -->
<html>
<head>
<style>
iframe {
width: 500px;
height: 500px;
position: absolute;
top: 0;
left: 0;
opacity: 0.0001; /* Nearly invisible */
z-index: 2;
}
button {
position: absolute;
top: 200px;
left: 200px;
z-index: 1;
}
</style>
</head>
<body>
<h1>点击下方按钮领取奖品!</h1>
<button>领取 $1000</button>
<!-- Hidden iframe positioned over the button -->
<iframe src="https://bank.com/transfer?to=attacker&amount=1000"></iframe>
<!-- 用户以为点击"领取奖品",实际点击了银行转账按钮 -->
</body>
</html>
func SecurityHeaders() gin.HandlerFunc {
return func(c *gin.Context) {
// Prevent page from being embedded in iframe
c.Header("X-Frame-Options", "DENY")
// Options: DENY, SAMEORIGIN
c.Next()
}
}
// Alternative: Allow iframe from same origin only
func AllowSameOriginFraming() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("X-Frame-Options", "SAMEORIGIN")
c.Next()
}
}
func SetCSPFrameAncestors() gin.HandlerFunc {
return func(c *gin.Context) {
// Modern replacement for X-Frame-Options
csp := "frame-ancestors 'none'" // Equivalent to X-Frame-Options: DENY
// csp := "frame-ancestors 'self'" // Equivalent to SAMEORIGIN
// csp := "frame-ancestors 'self' https://trusted.com" // Allow specific origins
c.Header("Content-Security-Policy", csp)
c.Next()
}
}
// Break out of iframe if page is framed
<script>
if (top !== self) {
top.location = self.location;
}
</script>
// More robust version
<script>
(function() {
if (top !== self) {
try {
if (top.location.hostname !== self.location.hostname) {
throw new Error('Clickjacking detected');
}
} catch(e) {
top.location = self.location;
}
}
})();
</script>
package middleware
import "github.com/gin-gonic/gin"
func SecurityHeaders() gin.HandlerFunc {
return func(c *gin.Context) {
// Clickjacking protection
c.Header("X-Frame-Options", "DENY")
c.Header("Content-Security-Policy", "frame-ancestors 'none'")
// XSS protection
c.Header("X-Content-Type-Options", "nosniff")
c.Header("X-XSS-Protection", "1; mode=block")
// HTTPS enforcement
c.Header("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload")
// Referrer policy
c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
// Permissions policy
c.Header("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
c.Next()
}
}
// ❌ VULNERABLE - No validation
func UploadFile(c *gin.Context) {
file, _ := c.FormFile("file")
// Dangerous: Save file without any checks
c.SaveUploadedFile(file, "./uploads/" + file.Filename)
c.JSON(200, gin.H{"message": "File uploaded"})
}
// Attacker uploads: shell.php, ../../etc/passwd, huge.zip
package fileupload
import (
"net/http"
"path/filepath"
"strings"
"mime/multipart"
"errors"
"github.com/gin-gonic/gin"
)
var allowedExtensions = map[string]bool{
".jpg": true,
".jpeg": true,
".png": true,
".gif": true,
".pdf": true,
".doc": true,
".docx": true,
}
var allowedMimeTypes = map[string]bool{
"image/jpeg": true,
"image/png": true,
"image/gif": true,
"application/pdf": true,
}
// ValidateFileType checks both extension and MIME type
func ValidateFileType(file *multipart.FileHeader) error {
// Check extension
ext := strings.ToLower(filepath.Ext(file.Filename))
if !allowedExtensions[ext] {
return errors.New("file type not allowed")
}
// Check MIME type by reading file header
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
buffer := make([]byte, 512)
_, err = src.Read(buffer)
if err != nil {
return err
}
mimeType := http.DetectContentType(buffer)
if !allowedMimeTypes[mimeType] {
return errors.New("MIME type not allowed")
}
return nil
}
const MaxFileSize = 10 * 1024 * 1024 // 10MB
func UploadFile(c *gin.Context) {
// Set max memory for multipart forms
c.Request.ParseMultipartForm(MaxFileSize)
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": "No file uploaded"})
return
}
// Check file size
if file.Size > MaxFileSize {
c.JSON(400, gin.H{"error": "File too large"})
return
}
// Validate file type
if err := ValidateFileType(file); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Process file...
}
// Set global limit in Gin
func main() {
r := gin.Default()
r.MaxMultipartMemory = MaxFileSize
// Routes...
}
import (
"crypto/md5"
"encoding/hex"
"path/filepath"
"time"
"strings"
"github.com/google/uuid"
)
// GenerateSafeFilename creates a safe, unique filename
func GenerateSafeFilename(originalFilename string) string {
// Get file extension
ext := filepath.Ext(originalFilename)
// Generate unique name using UUID
uniqueName := uuid.New().String()
return uniqueName + ext
}
// Alternative: Use hash + timestamp
func GenerateHashedFilename(originalFilename string) string {
ext := filepath.Ext(originalFilename)
hash := md5.Sum([]byte(originalFilename + time.Now().String()))
hashStr := hex.EncodeToString(hash[:])
return hashStr + ext
}
// Sanitize user-provided filename
func SanitizeFilename(filename string) string {
// Remove path separators
filename = filepath.Base(filename)
// Remove dangerous characters
dangerous := []string{"..", "/", "\\", "\x00", ":", "*", "?", "\"", "<", ">", "|"}
for _, char := range dangerous {
filename = strings.ReplaceAll(filename, char, "")
}
return filename
}
import (
"os"
"time"
)
type FileUpload struct {
ID uint `json:"id"`
OriginalName string `json:"original_name"`
StoredName string `json:"stored_name"`
Size int64 `json:"size"`
MimeType string `json:"mime_type"`
UserID uint `json:"user_id"`
UploadedAt time.Time `json:"uploaded_at"`
}
func SecureFileUpload(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": "No file uploaded"})
return
}
// Validate file
if err := ValidateFileType(file); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
if file.Size > MaxFileSize {
c.JSON(400, gin.H{"error": "File too large"})
return
}
// Generate safe filename
safeFilename := GenerateSafeFilename(file.Filename)
// Store outside web root
uploadDir := "/var/app/uploads" // Outside public directory
filePath := filepath.Join(uploadDir, safeFilename)
// Ensure directory exists
os.MkdirAll(uploadDir, 0755)
// Save file
if err := c.SaveUploadedFile(file, filePath); err != nil {
c.JSON(500, gin.H{"error": "Failed to save file"})
return
}
// Save metadata to database
fileRecord := FileUpload{
OriginalName: SanitizeFilename(file.Filename),
StoredName: safeFilename,
Size: file.Size,
MimeType: file.Header.Get("Content-Type"),
UserID: getCurrentUserID(c),
UploadedAt: time.Now(),
}
db.Create(&fileRecord)
c.JSON(200, gin.H{
"message": "File uploaded successfully",
"file_id": fileRecord.ID,
})
}
func getCurrentUserID(c *gin.Context) uint {
userID, _ := c.Get("userID")
return userID.(uint)
}
func DownloadFile(c *gin.Context) {
fileID := c.Param("id")
userID := getCurrentUserID(c)
// Get file record from database
var fileRecord FileUpload
if err := db.Where("id = ? AND user_id = ?", fileID, userID).First(&fileRecord).Error; err != nil {
c.JSON(404, gin.H{"error": "File not found"})
return
}
// Construct file path
filePath := filepath.Join("/var/app/uploads", fileRecord.StoredName)
// Verify file exists and is within allowed directory
absPath, err := filepath.Abs(filePath)
if err != nil || !strings.HasPrefix(absPath, "/var/app/uploads") {
c.JSON(403, gin.H{"error": "Access denied"})
return
}
// Set headers for download
c.Header("Content-Description", "File Transfer")
c.Header("Content-Transfer-Encoding", "binary")
c.Header("Content-Disposition", "attachment; filename="+fileRecord.OriginalName)
c.Header("Content-Type", fileRecord.MimeType)
// Serve file
c.File(absPath)
}
import "github.com/dutchcoders/go-clamd"
import "fmt"
func ScanFile(filePath string) (bool, error) {
c := clamd.NewClamd("/var/run/clamav/clamd.sock")
response, err := c.ScanFile(filePath)
if err != nil {
return false, err
}
// Check if virus was found
for _, result := range response {
if result.Status == "FOUND" {
return false, fmt.Errorf("virus detected: %s", result.Description)
}
}
return true, nil
}
func SecureUploadWithScan(c *gin.Context) {
// ... previous upload code ...
// Scan file for viruses
isClean, err := ScanFile(filePath)
if err != nil || !isClean {
os.Remove(filePath) // Delete infected file
c.JSON(400, gin.H{"error": "File failed security scan"})
return
}
c.JSON(200, gin.H{"message": "File uploaded successfully"})
}
// ❌ VULNERABLE - Fetching user-provided URL without validation
func FetchURL(c *gin.Context) {
url := c.Query("url")
// Dangerous: Server makes request to attacker-controlled URL
resp, _ := http.Get(url)
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
c.Data(200, "text/html", body)
}
// Attacker exploits:
// ?url=http://localhost:6379 (Access Redis)
// ?url=http://169.254.169.254/latest/meta-data/ (AWS metadata)
// ?url=file:///etc/passwd (Read local files)
import (
"net/url"
"errors"
)
var allowedHosts = map[string]bool{
"api.example.com": true,
"cdn.example.com": true,
"partner.trusted.com": true,
}
func ValidateURL(urlStr string) error {
parsedURL, err := url.Parse(urlStr)
if err != nil {
return errors.New("invalid URL")
}
// Only allow HTTPS
if parsedURL.Scheme != "https" {
return errors.New("only HTTPS allowed")
}
// Check against whitelist
if !allowedHosts[parsedURL.Host] {
return errors.New("host not allowed")
}
return nil
}
func SafeFetchURL(c *gin.Context) {
urlStr := c.Query("url")
if err := ValidateURL(urlStr); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Safe to fetch
resp, err := http.Get(urlStr)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to fetch URL"})
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
c.Data(200, resp.Header.Get("Content-Type"), body)
}
import (
"net"
"time"
)
var blockedIPRanges = []string{
"127.0.0.0/8", // Localhost
"10.0.0.0/8", // Private network
"172.16.0.0/12", // Private network
"192.168.0.0/16", // Private network
"169.254.0.0/16", // Link-local
"::1/128", // IPv6 localhost
"fc00::/7", // IPv6 private
}
func IsIPBlocked(ipStr string) bool {
ip := net.ParseIP(ipStr)
if ip == nil {
return true
}
for _, cidr := range blockedIPRanges {
_, network, _ := net.ParseCIDR(cidr)
if network.Contains(ip) {
return true
}
}
return false
}
func SafeHTTPClient(urlStr string) (*http.Response, error) {
parsedURL, err := url.Parse(urlStr)
if err != nil {
return nil, err
}
// Resolve hostname to IP
ips, err := net.LookupIP(parsedURL.Hostname())
if err != nil {
return nil, err
}
// Check if any resolved IP is blocked
for _, ip := range ips {
if IsIPBlocked(ip.String()) {
return nil, errors.New("access to internal IP blocked")
}
}
// Create custom transport with timeout
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 5 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 5 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
client := &http.Client{
Transport: transport,
Timeout: 10 * time.Second,
}
return client.Get(urlStr)
}
import "fmt"
func ValidateProtocol(urlStr string) error {
parsedURL, err := url.Parse(urlStr)
if err != nil {
return err
}
// Only allow HTTP and HTTPS
allowedSchemes := map[string]bool{
"http": true,
"https": true,
}
if !allowedSchemes[parsedURL.Scheme] {
return fmt.Errorf("protocol '%s' not allowed", parsedURL.Scheme)
}
return nil
}
// Prevent dangerous protocols like file://, gopher://, dict://, etc.
package ssrf
import (
"net"
"net/http"
"time"
"io"
)
// CreateSecureHTTPClient returns a configured HTTP client
func CreateSecureHTTPClient() *http.Client {
dialer := &net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 5 * time.Second,
}
transport := &http.Transport{
DialContext: dialer.DialContext,
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 5 * time.Second,
// Disable redirect following to prevent bypass
DisableRedirects: true,
}
return &http.Client{
Transport: transport,
Timeout: 10 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse // Don't follow redirects
},
}
}
func SecureFetch(urlStr string) ([]byte, error) {
// Validate URL
if err := ValidateURL(urlStr); err != nil {
return nil, err
}
if err := ValidateProtocol(urlStr); err != nil {
return nil, err
}
// Use secure client
client := CreateSecureHTTPClient()
resp, err := client.Get(urlStr)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Limit response size to prevent DoS
limitedReader := io.LimitReader(resp.Body, 10*1024*1024) // 10MB max
body, err := io.ReadAll(limitedReader)
if err != nil {
return nil, err
}
return body, nil
}
// ✅ SAFE - Use JSON for data exchange
func ProcessUserData(c *gin.Context) {
var user User
// JSON unmarshaling is safer than binary deserialization
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": "Invalid input"})
return
}
// Validate data
if err := validateUser(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Process user...
}
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
)
var secretKey = []byte("your-secret-key-change-this")
// Sign data with HMAC
func SignData(data []byte) string {
h := hmac.New(sha256.New, secretKey)
h.Write(data)
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
// Verify HMAC signature
func VerifySignature(data []byte, signature string) bool {
expectedMAC, _ := base64.StdEncoding.DecodeString(signature)
h := hmac.New(sha256.New, secretKey)
h.Write(data)
actualMAC := h.Sum(nil)
return hmac.Equal(actualMAC, expectedMAC)
}
// Secure serialization with integrity check
func SerializeSecurely(data interface{}) (string, error) {
jsonData, err := json.Marshal(data)
if err != nil {
return "", err
}
signature := SignData(jsonData)
result := map[string]string{
"data": base64.StdEncoding.EncodeToString(jsonData),
"signature": signature,
}
resultJSON, _ := json.Marshal(result)
return string(resultJSON), nil
}
// Secure deserialization with integrity check
func DeserializeSecurely(input string, target interface{}) error {
var container map[string]string
if err := json.Unmarshal([]byte(input), &container); err != nil {
return err
}
data, _ := base64.StdEncoding.DecodeString(container["data"])
signature := container["signature"]
// Verify integrity
if !VerifySignature(data, signature) {
return errors.New("signature verification failed")
}
return json.Unmarshal(data, target)
}