From: Tom Danielsen Date: 2003-05-30T10:38:41+09:00 Subject: Re: [Q]: CGI::Session On 30.05 04:02, Anders Borch wrote: | gabriele renzi wrote: | >il Thu, 29 May 2003 08:14:35 +0900, Tom Danielsen ha | >scritto:: | > | > | >>A couple of questions about CGI::Session : | >> | >>- why is only the first part of the MD5 sum used in | >>the session_id ? | | someone (a russian security expert) told me that you can safely use a | substring of an MD5 sum in stead of the entire MD5 sum since every bit | in the MD5 sum will change value if the source data changes. As far as I know any 64-bit part of the hash is as good as any other. But why not use the full 128-bit hash? Performance? I modified session.rb to use the full hash, and it seems to work ok. (Better collision resistance). | | >> | >>- why does CGI::Session::FileStore write the session | >>variables as text, instead of using Marshal ? | >> | | good question. Well, I had to try.... but I ran into something that I don't understand. Perhaps 0330 is not the time my brain is at it's best? I created a MarshaledFileStore class (slightly modified FileStore), but it won't work for some mysterious reason : [Sat May 31 03:26:37 2003] [error] mod_ruby: error in ruby /usr/local/lib/ruby/1.8/cgi/session.rb:155:in `dump': singleton can't be dumped (TypeError) from /usr/local/lib/ruby/1.8/cgi/session.rb:155:in `update' from /usr/local/lib/ruby/1.8/cgi/session.rb:161:in `close' from /usr/local/lib/ruby/1.8/cgi/session.rb:96:in `close' from /www/htdocs/app1.rbx:45 from /usr/local/lib/ruby/1.8/apache/ruby-run.rb:70:in `load' from /usr/local/lib/ruby/1.8/apache/ruby-run.rb:70:in `handler' This is what the hash looks like (output from my modified session.rb) : /www% more /tmp/h.h class of @hash is 'Hash' {"name"=>"test", "visited"=>8} 'name' (String) => 'test' (String) 'visited' (String) => '8' (Fixnum) /www% So......singleton ?? And here is the modified session.rb : regards, Tom ------------- cut ------------- # Copyright (C) 2001 Yukihiro "Matz" Matsumoto # Copyright (C) 2000 Network Applied Communication Laboratory, Inc. # Copyright (C) 2000 Information-technology Promotion Agency, Japan require 'cgi' class CGI class Session attr_reader :session_id def Session::callback(dbman) lambda{ dbman[0].close unless dbman.empty? } end def Session::create_new_id require 'digest/md5' md5 = Digest::MD5::new md5.update(String(Time::now)) md5.update(String(rand(0))) md5.update(String($$)) md5.update('foobar') ### TOM md5.hexdigest[0,16] md5.hexdigest end def initialize(request, option={}) session_key = option['session_key'] || '_session_id' id, = option['session_id'] unless id if option['new_session'] id = Session::create_new_id end end unless id id, = request[session_key] id = id.read if id.respond_to?(:read) unless id id, = request.cookies[session_key] end unless id if option.key?('new_session') and not option['new_session'] raise ArgumentError, "session_key `%s' should be supplied"%session_key end id = Session::create_new_id end end @session_id = id dbman = option['database_manager'] || FileStore @dbman = dbman::new(self, option) request.instance_eval do @output_hidden = {session_key => id} @output_cookies = [ Cookie::new("name" => session_key, "value" => id, "expires" => option['session_expires'], "domain" => option['session_domain'], "secure" => option['session_secure'], "path" => if option['session_path'] then option['session_path'] elsif ENV["SCRIPT_NAME"] then File::dirname(ENV["SCRIPT_NAME"]) else "" end) ] end @dbprot = [@dbman] ObjectSpace::define_finalizer(self, Session::callback(@dbprot)) end def [](key) unless @data @data = @dbman.restore end @data[key] end def []=(key, val) unless @write_lock @write_lock = true end unless @data @data = @dbman.restore end @data[key] = val end def update @dbman.update end def close @dbman.close @dbprot.clear end def delete @dbman.delete @dbprot.clear end class MarshaledFileStore def check_id(id) /[^0-9a-zA-Z]/ =~ id.to_s ? false : true end def initialize(session, option={}) dir = option['tmpdir'] || ENV['TMP'] || '/tmp' prefix = option['prefix'] || '' id = session.session_id unless check_id(id) raise ArgumentError, "session_id `%s' is invalid" % id end path = dir+"/"+prefix+id path.untaint unless File::exist? path @hash = {} end begin @f = open(path, "r+") rescue Errno::ENOENT @f = open(path, "w+") end end def restore unless @hash @hash = {} @f.flock File::LOCK_EX @f.rewind @hash = Marshal.load(@f) end @hash end def update return unless @hash @f.rewind File.open("/tmp/h.h","w+") { |f| f.print "class of @hash is '#{@hash.class.to_s}'\n" f.print @hash.inspect + "\n" @hash.each { |k,v| f.print "'#{k.to_s}' (#{k.class.to_s}) => '#{v.to_s}' (#{v.class.to_s})\n" } } Marshal.dump(@hash, @f) @f.truncate @f.tell end def close return if @f.closed? update @f.close end def delete path = @f.path @f.close File::unlink path end end class FileStore def check_id(id) /[^0-9a-zA-Z]/ =~ id.to_s ? false : true end def initialize(session, option={}) dir = option['tmpdir'] || ENV['TMP'] || '/tmp' prefix = option['prefix'] || '' id = session.session_id unless check_id(id) raise ArgumentError, "session_id `%s' is invalid" % id end path = dir+"/"+prefix+id path.untaint unless File::exist? path @hash = {} end begin @f = open(path, "r+") rescue Errno::ENOENT @f = open(path, "w+") end end def restore unless @hash @hash = {} @f.flock File::LOCK_EX @f.rewind for line in @f line.chomp! k, v = line.split('=',2) @hash[CGI::unescape(k)] = CGI::unescape(v) end end @hash end def update return unless @hash @f.rewind for k,v in @hash @f.printf "%s=%s\n", CGI::escape(k), CGI::escape(String(v)) end @f.truncate @f.tell end def close return if @f.closed? update @f.close end def delete path = @f.path @f.close File::unlink path end end class MemoryStore GLOBAL_HASH_TABLE = {} def initialize(session, option=nil) @session_id = session.session_id GLOBAL_HASH_TABLE[@session_id] ||= {} end def restore GLOBAL_HASH_TABLE[@session_id] end def update # don't need to update; hash is shared end def close # don't need to close end def delete GLOBAL_HASH_TABLE.delete(@session_id) end end end end