guest@websec: ~/websec/18-intranet-and-admin/th — zsh — 100×40 เขียนโดย SaKKo ENTH โค้ด:

guest@websec ~ % man 18-intranet-and-admin

18 · ความปลอดภัยของ intranet และระบบ admin

เครื่องมือ admin มีอำนาจมากที่สุด สร้างเร็วที่สุด และถูกตรวจน้อยที่สุด ผู้โจมตีรู้ดี ยึด admin ได้คนเดียวก็ได้ผู้ใช้ทุกคน

ทำไมระบบ admin จึงพิเศษ

หน้า admin ทำได้ทุกอย่าง และแสดงสิ่งที่คนทั่วไปพิมพ์เข้ามา: ticket โปรไฟล์ ชื่อไฟล์ เหมาะกับ XSS มาก

"ระบบภายใน" ไม่ใช่กำแพง เบราว์เซอร์ของพนักงานก็ใช้อ่านอีเมลและท่องเว็บด้วย

18.1 Cross-site scripting ในเครื่องมือ admin

ฟิลด์ที่ไม่ได้ escape ช่องเดียวในหน้า admin ก็พอ สคริปต์รันในนาม admin และแอบสร้าง admin ใหม่ได้

ทางแก้เดียวกับหัวข้อ 04 แต่ต้องใช้กับเครื่องมือภายใน เข้มงวดเท่ากัน

18.2 CSRF ต่อผู้ดูแลระบบ

CSRF ต่อ admin ทำได้ทุกอย่างที่ admin ทำได้ ตัวอย่างจริง: หน้าเว็บที่แอบเปลี่ยนการตั้งค่าเราเตอร์ที่บ้านของคนอื่น

ป้องกันแบบเดียวกับหัวข้อ 07 อย่าปิด CSRF "เพราะเป็นของภายใน"

18.3 ข้อควรระวังเพิ่มเติม

วางแผนสำหรับวันที่บัญชี admin ถูกยึด:

sim: เสริมความแข็งแรงให้ระบบ adminผู้โจมตีได้ session ของพนักงานซัพพอร์ต จะไปได้ไกลแค่ไหน?

ขอบเขตความเสียหาย

  • บทบาท ซัพพอร์ตไม่ต้องมีสิทธิ์ของการเงิน ตรวจที่เซิร์ฟเวอร์ทุกครั้ง
  • แยกบัญชี admin ใช้ passkey หรือ security key
  • ถามยืนยันอีกครั้ง ก่อนทำสิ่งอันตราย เช่น export หรือเปลี่ยนสิทธิ์
  • ใช้ได้เฉพาะผ่าน VPN หรือเครือข่ายออฟฟิศ
  • แยก subdomain เช่น admin.example.com XSS บนเว็บสาธารณะจะเอื้อมไม่ถึงคุกกี้ admin
  • Audit log + การแจ้งเตือน ทุกการกระทำที่ใช้สิทธิ์สูง
ไม่มีอะไรในตัว gem admin escape ให้ แต่บทบาท จำกัด IP ยืนยันซ้ำ และ audit log เป็นของเรา (ลอง pundit กับ audited)
# config/routes.rb: admin only on its own host
constraints(host: "admin.example.com") do
  namespace :admin do
    resources :users
  end
end

class Admin::BaseController < ApplicationController
  ADMIN_NET = IPAddr.new("10.0.0.0/8")             # VPN range (trust your proxy config)
  before_action { head :forbidden unless ADMIN_NET.include?(request.remote_ip) }
  before_action :require_admin_session!            # separate admin login + MFA
  content_security_policy { |p| p.default_src :self; p.script_src :self }
end

class Admin::RolesController < Admin::BaseController
  before_action -> { require_recent_auth!(5.minutes) }   # your helper: re-prompt
  def update
    user = User.find(params[:user_id])
    authorize user, :change_role?                  # Pundit policy
    user.update!(role: params.require(:role))
    AuditLog.create!(actor: Current.admin, action: "role_change", target: user)
  end
end
// Express: lock the admin router down in layers
const admin = express.Router();
admin.use(requireNetwork(["10.0.0.0/8"]));          // VPN range (behind a trusted proxy)
admin.use(requireAdminSession);                       // separate admin login + MFA
admin.use(helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"] } }));

admin.post("/users/:id/role", requireRole("superadmin"), requireRecentAuth(5 * 60),
  async (req, res) => {
    await audit.log(req.admin.id, "role_change", req.params.id, req.body.role);
    …
  });
app.use(vhost("admin.example.com", admin));            // separate host
# Django: staff-only views, permissions, and re-auth for sensitive actions
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import permission_required

@staff_member_required
@permission_required("accounts.change_role", raise_exception=True)
@require_recent_auth(minutes=5)          # your decorator: redirect to re-auth
def change_role(request, user_id):
    AuditLog.objects.create(actor=request.user, action="role_change", target=user_id)
    ...

# Serve the admin on its own host, e.g. with django-hosts or a separate
# deployment, and restrict it at the proxy (nginx allow/deny, IAP, VPN).