Halaman

Jumat, 05 Februari 2010

[shared] Directory Traversal in Rails

beberapa saat yang lalu di project yang sedang saia kerjakan, saia menyadari sebuah security hole yang bisa dibilang cukup berbahaya (untungnya sebelum terjadi hal2 yang tidak diinginkan). yang menyebabkan masalah, saia membuat sebuah page yang isinya berasal dari partial berdasarkan params, isi view-nya kurang lebih seperti ini:

# views/frontpages/show.html.erb
<h3>Title</h3>
<%= render :partial => params[:page] || "home" %>

jadi isi dari page tersebut tergantung dari params page. yang jadi masalah ketika user memasukkan suatu url seperti
http://..../frontpages/show?page=%2fadmin%2fusers%2flist
(url dapat dengan mudah dicari menggunakan dictionary attack atau sejenisnya)
maka partial yang di-load akan berasal dari halaman di admin::users.
walaupun besar kemungkinan user tidak akan dapat melihat data di halaman tersebut karena akses tidak melalui controller, tetapi hal tersebut tetap tidak seharusnya diperbolehkan. apalagi terkadang dengan alasan 'malas' atau lainnya kita melakukan pengambilan data tidak di dalam controller tetapi langsung di view atau helper dimana data tersebut digunakan (ini salah satu alasan mengapa pengambilan data harus berada di controller bukan di view)

untuk mengatasi hal tersebut saia memproses params page td sebelum digunakan di view, misalkan:

# /controllers/frontpages_controller.rb
# ...
def show
@partial = (params[:page].blank? ||
params[:page].try('include?', '/')) ?
'home' : params[:page]
end
# ...


# views/frontpages/show.html.erb
<h3>Title</h3>
<%= render :partial => @partial %>

atau lebih baik lagi dengan tidak meng-implementasi partial seperti ini dan membuat action terpisah untuk setiap page.

yah situasi di atas mungkin merupakan hal bodoh yang seharusnya tidak terjadi selama kita mengikuti konvensi dari rails, tp ada kalanya karena berbagai alasan kita melanggar konvensi dan menggunakan implementasi kita sendiri, karenanya sebaiknya kita berpikir 1000x (lebay) sebelum melakukan hal itu.

oh ya directory traversal sendiri bukan hanya dapat digunakan pada situasi seperti di atas, tetapi banyak situasi lain yang bisa dimanfaatkan dengan directory traversal ini. karena itu, berhati-hatilah, inget kata bang napi (halah..)

ok segitu aja, tadinya mao nulis lanjutan dari svn branching tapi ditunda buat next post ^_^

Rabu, 03 Februari 2010

[shared] Branching using TortoiseSVN part 1

skarang saia akan sedikit sharing tentang branching dengan tortoise svn. perlu saia beritahukan, implementasi yang saia gunakan mungkin bukan yang terbaik, karena itu kalo ada yg mempunyai implementasi lebih baik tolong beritahu saia melalui comment ^_^


Buat branch


pertama yg perlu dilakukan buat 3 folder di dalam folder project, yaitu: branches, tags dan trunk

branching -- trunk
-- branches
-- tags


kemudian taruh project anda di dalam folder trunk, folder trunk ini akan digunakan sebagai kode stabil yang digunakan di production server.

branching -- trunk -- app
-- config
-- db
-- dll..
-- branches
-- tags


commit, lalu klik kanan di folder trunk, pilih menu TortoiseSVN>Branch/tag ubah To URL menjadi "../branches/staging", tambahkan message kalo perlu, dan OK.

setelah selesai update folder staging, maka staging akan ditambahkan

branching -- trunk -- app
-- config
-- db
-- dll..
-- branches -- staging -- app
-- config
-- dll..
-- tags

folder staging ini digunakan untuk kode staging server, biasa saia gunakan untuk kode2 yg perlu ditest atau dicek oleh klien sebelum masuk production.


Merge


setelah melakukan banyak update2 di staging, dan klien menyatakan ok untuk suatu update/feature agar di-deploy ke production, kita dapat dengan mudah menggunakan fitur merge.

Perhatian! untuk mempermudah merging, ikuti aturan commit di akhir post ini..

pastikan fitur yg akan di merge sudah di commit, kemudian klik kanan di folder trunk, pilih menu TortoiseSVN>Merge, pilih "Merge a range of revisions", masukkan url staging dalam URL to merge from, dan tentukan revision dari fitur yg akan di-merge, sebaiknya gunakan show log.
dalam log pilih revision yang akan di-merge, revision yang sudah pernah di-merge akan berwarna abu2, next lalu merge
stelah selesai periksa kode yang ditambahkan, bila ada kesalahan dalam memilih revision gunakan revert pada trunk. sebaiknya jangan pernah secara langsung merubah trunk, kalau ada kesalahan dalam kode, tambahkan perbaikan di staging dan merge ke trunk.

commit dan selesai.


Tambahan


beberapa tambahan yang perlu diperhatikan:

  • Hindari meng-edit trunk secara langsung

  • Tambahkan message yang jelas dalam setiap commit di staging

  • Usahakan agar setiap update yang ditambahkan di staging selesai dalam sesedikit mungkin commit, kalau fitur yang ditambahkan cukup besar dan rumit, gunakan feature branch (akan dibahas di post berikutnya)

  • Jangan pernah menambahkan 2 fitur dalam 1 revision, pisahkan dalam revision yang berbeda, akan menyulitkan pada saat memilih revision untuk di-merge nantinya



ok segitu dulu, post berikutnya saia akan membahas feature branches dan tags

Senin, 01 Februari 2010

[shared] nested layout

sesuai dengan yang saia janjikan di post sebelumnya, skarang saia akan membahas soal nested layout.
untuk memudahkan, saia akan mengambil contoh kasus dari post saia sbelumnya, yaitu untuk membuat menu yg berbeda2 tergantung controller yg aktif.


# layouts/application.html.erb
# ...
<div class="menu">
<%= render :partial => "main_menu" %>
</div>
<div class="content">
<%= yield %>
</div>
# ...


diubah menjadi:


# layouts/application.html.erb
# ...
<div class="menu">
<% begin %>
<%= render :partial => "layouts/#{controller.controller_path}_menu" %>
<% rescue ActionView::MissingTemplate %>
<% begin %>
<%= render :partial => "layouts/#{controller.controller_path.sub(/#{controller.controller_name}$/,'default_menu')}" %>
<% rescue ActionView::MissingTemplate %>
<%= render :partial => "main_menu" %>
<% end %>
<% end %>
</div>
<div class="content">
<%= yield %>
</div>
# ...


kode diatas akan mencari layout menu dengan path dan nama controller yg sedang aktif, bila tidak ditemukan default_menu partial dalam path controller aktif akan digunakan, bila tidak ada juga maka main_menu sebagai default akan digunakan. contoh:
  • UsersController akan menggunakan "layouts/_users_menu" || "layouts/_default_menu" || "layouts/_main_menu"
  • Admin::UsersController akan menggunakan "layouts/admin/_users_menu" || "layouts/admin/_default_menu" || "layouts/_main_menu" 
  • Admin::HomeController akan menggunakan "layouts/admin/_home_menu" || "layouts/admin/_default_menu" || "layouts/_main_menu" 
jadi untuk menggunakan menu yang sama pada smua controller untuk admin, buat partial "layouts/admin/_default_menu" dan tentu saja bila dibutuhkan tetap dapat di override dengan "layouts/admin/_[nama_controller]_menu"

dengan menggunakan nested layout, kita cukup menggunakan hanya 1 application layout. walaupun lebih tidak flexible daripada menggunakan content_for yg dapat diatur bergantung pada action. tp menurut saia jauh lebih rapih karena mengurangi kode yg ditulis pada controller.

Rabu, 20 Januari 2010

[shared] Using content_for from controller

biasanya content_for digunakan untuk passing suatu blok text dr view ke layout, contohnya

layout/application.html.erb

# ...
<div class="menu">
<%= yield :menu %>
</div>
<div class="content">
<%= yield %>
</div>
# ...


homepages/show.html.erb

# ...
<% content_for :menu, generate_menu %>
# ...


nah, terkadang dibutuhkan content_for yg dapat dipanggil di controller. misalkan untuk mengurangi pengulangan dengan menggunakan before_filter (dapat jg menggunakan nested layout, akan saia bahas di post lain).
untuk itu setelah saia coba ternyata hal tersebut dapat diselesaikan dengan mudah, hanya dengan mengisi instance variable dengan nama @content_for_#{nama} dengan contoh diatas berarti:

homepages_controller.rb

# ...
@content_for_menu = generate_menu
# ...


Warning: cara tersebut sudah dicoba di rails 2.3.3 tapi ada kmungkinan deprecated untuk versi rails yang lebih baru.. kalo ada cara yg lebih baik tolong kasi tau saia via comment, thanks ^_^

Jumat, 15 Januari 2010

[shared] Simple scheduled emails using rufus-scheduler

Baru2 ini di project yg sedang saia kerjakan, saia diminta untuk mengimplementasikan automated email. Intinya mengirim email secara otomatis pada saat user melakukan sesuatu. Sekilas biasa saja, hanya menggunakan ActionMailer untuk mengirim email pada event tertentu, namun masalahnya ada beberapa email yang dikirim bukan pada saat event dijalankan, ada delay waktu bbrp hari sebelum email dikirim.

Untuk itu saia memutuskan untuk membuat mailer yang dapat dijadwalkan, selain dapat mengirimkan email pada tanggal dan waktu tertentu, email jg dikirimkan secara berkala pada jangka waktu yg ditentukan (asumsi saia akan dibutuhkan nantinya untuk email laporan berkala.)

Buat Scheduler

Pertama, install rufus-scheduler

gem install rufus-scheduler

lalu dalam folder config/initializers buat file ruby baru, misal "task_scheduler.rb"

scheduler = Rufus::Scheduler.start_new

scheduler.every("10m") do
QueuedEmail.deliver_all
end

yang akan menjalankan deliver_all pada model QueuedEmail setiap 10 menit.

Buat Model utk menyimpan jadwal

generate model baru, misal queued_email
migration:

class CreateQueuedEmails < ActiveRecord::Migration
def self.up
create_table :queued_emails do |t|
t.string :from, :limit => 100
t.string :recipients, :limit => 100
t.string :subject, :limit => 100
t.text :content
t.integer :duration
t.datetime :send_at
t.datetime :last_sent_at
t.timestamps
end
end

def self.down
drop_table :queued_emails
end
end

model:

class QueuedEmail < ActiveRecord::Base
validates_presence_of :from
validates_length_of :from, :within => 6..100

validates_presence_of :recipients
validates_length_of :recipients, :within => 6..100

def recurring?
!self.duration.blank?
end

def deliver!
puts "Sending email: #{self.inspect}"
UserMailer.deliver_custom_mail self.attributes
if recurring?
puts "set sent at: #{Time.now.utc}"
self.update_attribute :last_sent_at, Time.now.utc
else
puts "destroy"
self.destroy
end
end

def self.deliver_all
mails = self.all :conditions => ["(send_at <= :current_time) AND (duration IS NULL OR duration = \"\" OR last_sent_at IS NULL OR TIME_TO_SEC(TIMEDIFF(:current_time, last_sent_at)) >= duration)", {:current_time => Time.now.utc}]
mails.each do |mail|
mail.deliver!
end
end
end

cara kerja:
- duration: bila diisi, setelah email dikirim, email tidak akan langsung dihapus, tetapi akan dikirimkan kembali setiap duration detik
- send_at: menentukan tanggal dan waktu email akan dikirim atau dikirim pertama kalinya bila duration ditentukan.
- last_sent_at: tidak perlu diisi, digunakan secara internal untuk email berkala
- lainnya: rasanya sudah cukup jelas, sama seperti actionmailer

Tambahkan custom_mail di mailer 

Saia menggunakan UserMailer < ActionMailer::Base untuk mengirimkan email, tambahkan kode berikut di dalamnya:

def custom_mail(options)
@recipients = options["recipients"]
@from = options["from"]
@subject = options["subject"]
@sent_on = Time.now
@body[:content] = options["content"]
end

dan buat custom_mail.erb di viewnya:

<%= @content %>


Contoh penggunaan

Misalkan untuk mengirimkan survey email 2 hari setelah user register. Tambahkan kode berikut di users_controller create:

# ....
# bila registrasi sukses daftarkan email untuk dikirim setelah 2 hari
QueuedEmail.create(
:send_at => 2.days.since(Time.now.utc),
:from => "\"Site Keren\" ",
:recipients => "#{@user.email}",
:subject => "Survey supaya keren" ,
:content => "Halo #{@user.name}, isi survey berikut supaya kamu makin keren gitu loh ....."
)
# ....

untuk email berkala, tentukan duration dalam detik, misal email yg dikirim tiap hari

:duration => 24.hours


Dan segitu saja, mungkin bukan implementasi yg terbaik, tp paling tidak bisa digunakan hehe..