[Q]: CGI::Session

A couple of questions about CGI::Session :

  • why is only the first part of the MD5 sum used in
    the session_id ?

  • why does CGI::Session::FileStore write the session
    variables as text, instead of using Marshal ?

Tom

I add my own question:

  • when the $TMP/file gets deleted?
···

il Thu, 29 May 2003 08:14:35 +0900, Tom Danielsen tom@mnemonic.no ha scritto::

A couple of questions about CGI::Session :

  • why is only the first part of the MD5 sum used in
    the session_id ?

  • why does CGI::Session::FileStore write the session
    variables as text, instead of using Marshal ?

Tom

Hi,

  • why is only the first part of the MD5 sum used in
    the session_id ?

Not to make file names too long (Perl’s CGI session does this too). I
believe it is secure enough.

  • why does CGI::Session::FileStore write the session
    variables as text, instead of using Marshal ?

Allow users to choose their own stringify method.

						matz.
···

In message “[Q]: CGI::Session” on 03/05/29, Tom Danielsen tom@mnemonic.no writes:

gabriele renzi wrote:

il Thu, 29 May 2003 08:14:35 +0900, Tom Danielsen tom@mnemonic.no 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.

  • why does CGI::Session::FileStore write the session
    variables as text, instead of using Marshal ?

good question.

Tom

I add my own question:

  • when the $TMP/file gets deleted?

AFAIK it doesn’t on my server I have this crontab:

10 * * * * find /tmp -amin +30 -user apache -type f -exec rm -f {} ;

/Anders

matz-

if it were cleaned up a bit, what would you think about including my (or a
better impl) of CGI::Session::Pstore in the standard lib?

i use it all the time for cgi scripts and have found it to be really useful.

-a

···

On Sat, 31 May 2003, Yukihiro Matsumoto wrote:

Hi,

In message “[Q]: CGI::Session” > on 03/05/29, Tom Danielsen tom@mnemonic.no writes:

  • why is only the first part of the MD5 sum used in
    the session_id ?

Not to make file names too long (Perl’s CGI session does this too). I
believe it is secure enough.

  • why does CGI::Session::FileStore write the session
    variables as text, instead of using Marshal ?

Allow users to choose their own stringify method.

  					matz.

Ara Howard
NOAA Forecast Systems Laboratory
Information and Technology Services
Data Systems Group
R/FST 325 Broadway
Boulder, CO 80305-3328
Email: ara.t.howard@fsl.noaa.gov
Phone: 303-497-7238
Fax: 303-497-7259
~ > ruby -e ‘p % ^) .intern’
====================================

gabriele renzi wrote:

il Thu, 29 May 2003 08:14:35 +0900, Tom Danielsen tom@mnemonic.no 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

···

On 30.05 04:02, Anders Borch wrote:

Hi,

  • why is only the first part of the MD5 sum used in
    the session_id ?

Not to make file names too long (Perl’s CGI session does this too). I
believe it is secure enough.

Ok, I see this point. However - for me performance is not an issue,
it would be nice if this behaviour was configurable. For my use this
is just an unneccesary reduction of security (even if security still
is good).

  • why does CGI::Session::FileStore write the session
    variables as text, instead of using Marshal ?

Allow users to choose their own stringify method.

  					matz.

matz-

if it were cleaned up a bit, what would you think about including my (or a
better impl) of CGI::Session::Pstore in the standard lib?

i use it all the time for cgi scripts and have found it to be really useful.

I do agree, this would be nice to have in the standard distribution.

CGI::Session::Pstore works great! Now if i could just figure out what
went wrong with my code… :slight_smile:

Tom
···

On 31.05 00:52, ahoward wrote:

On Sat, 31 May 2003, Yukihiro Matsumoto wrote:

In message “[Q]: CGI::Session” > > on 03/05/29, Tom Danielsen tom@mnemonic.no writes:

Hi,

···

In message “Re: [Q]: CGI::Session” on 03/05/31, ahoward ahoward@fsl.noaa.gov writes:

if it were cleaned up a bit, what would you think about including my (or a
better impl) of CGI::Session::Pstore in the standard lib?

i use it all the time for cgi scripts and have found it to be really useful.

Sounds nice. Let me see.

						matz.

I created a MarshaledFileStore class (slightly modified FileStore),
but it won’t work for some mysterious reason :

i did something similar a while back - i’ve used in many cgi programs and it
works great

http://raa.ruby-lang.org/list.rhtml?name=cgi-sess-pstore

-a

[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

-a

···

On Fri, 30 May 2003, Tom Danielsen wrote:

====================================

Ara Howard
NOAA Forecast Systems Laboratory
Information and Technology Services
Data Systems Group
R/FST 325 Broadway
Boulder, CO 80305-3328
Email: ara.t.howard@fsl.noaa.gov
Phone: 303-497-7238
Fax: 303-497-7259
~ > ruby -e ‘p % ^) .intern’
====================================

If you are using 64 bit hashes, then you need to have about 2^32 concurrent
sessions to get a reasonably high chance of a collision. That’s quite a lot
of sessions :slight_smile:

···

On Fri, May 30, 2003 at 10:38:41AM +0900, Tom Danielsen wrote:

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).