FreeBSD Bugzilla – Attachment 7499 Details for
Bug 16244
[PATCH] don't allow password re-use when changing passwords
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
Remember
[x]
|
Forgot Password
Login:
[x]
file.shar
file.shar (text/plain), 22.08 KB, created by
Scott Gasch
on 2000-01-21 06:00:01 UTC
(
hide
)
Description:
file.shar
Filename:
MIME Type:
Creator:
Scott Gasch
Created:
2000-01-21 06:00:01 UTC
Size:
22.08 KB
patch
obsolete
># This is a shell archive. Save it in a file, remove anything before ># this line, and then unpack it by entering "sh file". Note, it may ># create directories; files and directories will be owned by you and ># have default permissions. ># ># This archive contains: ># ># passhist.h ># passhist.c ># passwd.diff ># >echo x - passhist.h >sed 's/^X//' >passhist.h << 'END-of-passhist.h' >X//+---------------------------------------------------------------------------- >X// >X// File: passhist.h >X// >X// Module: passwd patch, FreeBSD. >X// >X// Synopsis: See passhist.c >X// >X//+---------------------------------------------------------------------------- >X >X#ifndef _PASSHIST_H_ >X#define _PASSHIST_H_ >X >X#include <limits.h> >X >X// >X// How many old passwords to remember, per user. >X// >X#define HIST_COUNT 3 >X >X// >X// Where to store encrupted old passwords. >X// >X#define HIST_FILE "/etc/passwd.hist" >X >X// >X// How long a password must be active (in sec) before it counts... this >X// is to stop people from writing scripts to change their password >X// HIST_COUNT times in a row and then reset the same original password. >X// >X#define HIST_TIME 3600 >X >Xtypedef struct _HIST_NODE >X{ >X uid_t uid; >X char szPass[HIST_COUNT][PASS_MAX]; >X time_t timeLastUpdate; >X int iNumPass; >X} >XHIST_NODE; >X >Xint InitializeList(void); >Xint ReadList(void); >Xint AddPass(uid_t uidUser, char *szPass); >Xint CheckPass(uid_t uidUser, char *szPass); >Xint WriteList(void); >Xvoid CloseList(void); >X >X#endif /* _PASSHIST_H_ */ >END-of-passhist.h >echo x - passhist.c >sed 's/^X//' >passhist.c << 'END-of-passhist.c' >X//+---------------------------------------------------------------------------- >X// >X// File: passhist.c >X// >X// Module: passwd patch, FreeBSD. >X// >X// Synopsis: A set of functions that maintain a password history file. >X// This file (HIST_FILE in passhist.h) stores a configurable >X// number of prior passwords for each user (HIST_COUNT in >X// passhist.h). These passwords are stored in encrypted format. >X// HIST_FILE's permissions and ownership are checked and reset >X// at every read/write operation so as to self-correct any >X// insecure setting an administrator has created. >X// >X// InitializeList and ReadList must be called before anything >X// else. >X// >X// Subsequent calls to CheckPass will determine whether a >X// proposed new password for a certain user has been used >X// by said user in his last PASS_COUNT passwords. >X// >X// Subsequent calls to AddPass will add a new password to >X// a specific user's list so long as he has had his current >X// password in effect for at least HIST_TIME seconds. This >X// is to prevent scripts from changing passwords HIST_COUNT >X// times in a row and then resetting the original password >X// again. >X// >X// Copyright (c) 2000 Scott Gasch (scott@wannabe.guru.org) >X// All rights reserved. >X// >X// Redistribution and use in source and binary forms, with or without >X// modification, are permitted provided that the following conditions >X// are met: >X// 1. Redistributions of source code must retain the above copyright >X// notice, this list of conditions and the following disclaimer. >X// 2. Redistributions in binary form must reproduce the above copyright >X// notice, this list of conditions and the following disclaimer in the >X// documentation and/or other materials provided with the distribution. >X// >X// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND >X// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE >X// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE >X// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE >X// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL >X// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS >X// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) >X// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT >X// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY >X// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF >X// SUCH DAMAGE. >X// >X//+---------------------------------------------------------------------------- >X >X#include <sys/types.h> >X#include <sys/param.h> >X#include <sys/stat.h> >X#include <unistd.h> >X#include <limits.h> >X#include <pwd.h> >X#include <stdlib.h> >X#include <stdio.h> >X#include <string.h> >X#include <time.h> >X >X#include "passhist.h" >X >X// >X// Local function protos. >X// >Xstatic HIST_NODE *GetHistEntry(uid_t uidTarg); >Xint CompareUids(const void *pA, const void *pB); >X >X// >X// History table and count of items in it. >X// >XHIST_NODE *g_pHist = NULL; >Xunsigned int g_iHistCount = 0; >X >X >X//+---------------------------------------------------------------------------- >X// >X// Function: CompareUids >X// >X// Synopsis: Compare two history table records (used by qsort). >X// >X// Arguments: const void *pA - ptr to first hist table record. >X// const void *pB - ptr to second hist table record. >X// >X// Returns: int - difference between records. >X// >X//+---------------------------------------------------------------------------- >Xint CompareUids(const void *pA, const void *pB) >X{ >X return (((HIST_NODE *) pA)->uid - ((HIST_NODE *) pB)->uid); >X} >X >X >X//+---------------------------------------------------------------------------- >X// >X// Function: InitializeList >X// >X// Synopsis: Prepare the memory history list for population by ReadList. >X// To do this we need to count the number of uids on the sys- >X// tem and allocate a big enough hunk of memory to hold the >X// list. We also populate the uid field of each record and >X// sort the memory list so that it is fast/easy for ReadList >X// to determine which uid is good later. >X// >X// Arguments: void >X// >X// Returns: int - 0 for failure, number of accounts (>0) for success. >X// >X//+---------------------------------------------------------------------------- >Xint InitializeList(void) >X{ >X struct passwd *pPwd; >X unsigned int iIndex = 0; >X >X // >X // Count the number of user account in the system. >X // >X setpwent(); >X g_iHistCount = 0; >X while ((pPwd = getpwent())) >X { >X g_iHistCount++; >X } >X >X // >X // Allocate a buffer large enough to hold a password history of N >X // user accounts. >X // >X if (NULL != g_pHist) free(g_pHist); >X g_pHist = (HIST_NODE *) malloc(g_iHistCount * sizeof(HIST_NODE)); >X if (NULL == g_pHist) >X { >X fprintf(stderr, "Out of memory.\n"); >X return(0); >X } >X memset(g_pHist, 0, g_iHistCount * sizeof(HIST_NODE)); >X >X // >X // Fill in the UID field in the history list. >X // >X setpwent(); >X while ((pPwd = getpwent())) >X { >X g_pHist[iIndex++].uid = pPwd->pw_uid; >X } >X endpwent(); >X >X // >X // Sort the list >X // >X qsort(g_pHist, g_iHistCount, sizeof(HIST_NODE), CompareUids); >X return(g_iHistCount); >X} >X >X >X//+---------------------------------------------------------------------------- >X// >X// Function: CloseList >X// >X// Synopsis: Cleanup... deallocate history table etc... >X// >X// Arguments: void >X// >X// Returns: void >X// >X//+---------------------------------------------------------------------------- >Xvoid CloseList(void) >X{ >X if (g_pHist) free(g_pHist); >X g_iHistCount = 0; >X} >X >X >X//+---------------------------------------------------------------------------- >X// >X// Function: CheckFilePerms >X// >X// Synopsis: Check that the HIST_FILE's owner is root and mode is >X// 0600. >X// >X// Arguments: void >X// >X// Returns: int - 0 if file perms are insecure, 1 if secure. >X// >X//+---------------------------------------------------------------------------- >Xint CheckFilePerms(void) >X{ >X struct stat sStat; // Stat buffer >X >X // >X // Stat it and check the info >X // >X if (0 != stat(HIST_FILE, &sStat)) >X { >X return(0); >X } >X >X if (sStat.st_uid) >X { >X return(0); >X } >X >X if (sStat.st_mode & (S_IRWXG | S_IRWXO)) >X { >X return(0); >X } >X >X return(1); >X} >X >X >X//+---------------------------------------------------------------------------- >X// >X// Function: ReadList >X// >X// Synopsis: Read in the HIST_FILE file... create a history list in >X// memory. For HIST_FILE line format see WriteList below. >X// Do not bother reading information about uids not valid >X// on the system anymore. >X// >X// Note: a HIST_PASS file line cannot be more than 2048 >X// characters wide. >X// >X// Arguments: void >X// >X// Returns: int - 0 on failure, 1 on success. >X// >X//+---------------------------------------------------------------------------- >Xint ReadList(void) >X{ >X FILE *fp = fopen(HIST_FILE, "r"); // File ptr. >X char buf[2048]; // A line from the file >X uid_t uid; // A uid from the file >X HIST_NODE *pEntry; // A hist entry >X char *pch; // Misc char ptr. >X >X // >X // If the file does not exist or there is some other read error, >X // don't bother. >X // >X if (NULL == fp) >X { >X return(0); >X } >X >X // >X // Print a nasty message to the user about his lax security... this >X // will notify him that something is wrong and that someone could >X // potentially be running a crack program on his user's old passwds. >X // Also, reset the file permissions. >X // >X if (0 == CheckFilePerms()) >X { >X fprintf(stderr, "Warning: resetting insecure owner/mode of %s.\n", >X HIST_FILE); >X chmod(HIST_FILE, S_IRUSR | S_IWUSR); >X chown(HIST_FILE, 0, 0); >X } >X >X // >X // Read a line from the file... we have a history table with >X // just uids in it, sorted, courtesy of InitializeList. This >X // lets us determine where the data from this line should be >X // put very quickly. >X // >X while(fgets(buf, 2048, fp)) >X { >X // >X // The uid should be the first thing on the line... >X // >X uid = (uid_t)atoi(buf); >X >X // >X // If there is no such user (we do not have a hist table node >X // already for him) skip it. >X // >X if (NULL == (pEntry = GetHistEntry(uid))) continue; >X >X // >X // Skip past the uid number to the first NULL and one char >X // beyond. >X // >X pch = buf; >X while (*pch) pch++; >X pch++; >X >X // >X // Read timestamp of last update.. the next thing on the line. >X // >X pEntry->timeLastUpdate = (time_t)atoi(pch); >X >X // >X // Skip until NULL and one beyond. >X // >X while (*pch) pch++; >X pch++; >X >X // >X // Read password list. >X // >X while (*pch) >X { >X if (pEntry->iNumPass < HIST_COUNT) >X { >X strncpy(pEntry->szPass[pEntry->iNumPass], pch, PASS_MAX); >X pEntry->iNumPass++; >X } >X >X while(*pch) pch++; >X pch++; >X } >X } >X fclose(fp); >X >X return(1); >X} >X >X >X//+---------------------------------------------------------------------------- >X// >X// Function: WriteList >X// >X// Synopsis: Write the list currently in memory out to the HIST_FILE file. >X// Each record is in the format: >X// >X// uid\0timestamp\0pass1\0pass2\0 ... passn\0\0\n >X// >X// uid: numeric uid for the user. >X// timestamp: seconds since epoch of last passwd update. >X// pass1..passn: encrypted old passwords. >X// >X// Arguments: void >X// >X// Returns: int - 0 on error, 1 on success. >X// >X//+---------------------------------------------------------------------------- >Xint WriteList(void) >X{ >X int i, j; // Loop control >X FILE *fp = fopen(HIST_FILE, "w"); // File ptr. >X >X // >X // Make sure the settings on the file are secure. >X // >X chmod(HIST_FILE, S_IRUSR | S_IWUSR); >X chown(HIST_FILE, 0, 0); >X >X if (!fp) return(0); >X >X for (i = 0; i < g_iHistCount; i++) >X { >X // >X // Only bother writing accounts for which we have saved passwd >X // info. >X // >X if (g_pHist[i].iNumPass) >X { >X fprintf(fp, "%d%c%d%c", g_pHist[i].uid, 0, >X (int)g_pHist[i].timeLastUpdate, 0); >X for (j = 0; j < g_pHist[i].iNumPass; j++) >X { >X fprintf(fp, "%s%c", g_pHist[i].szPass[j], 0); >X } >X fprintf(fp, "%c\n", 0); >X } >X } >X >X fclose(fp); >X return(1); >X} >X >X >X//+---------------------------------------------------------------------------- >X// >X// Function: AddPass >X// >X// Synopsis: Add a new (encrypted) password to a user's history. >X// >X// Arguments: uid_t uidUser - The uid of the guy whose history we update. >X// char *szPass - The (encrypted) password he has set. >X// >X// Returns: int - 1 on success and 0 on failure. >X// >X//+---------------------------------------------------------------------------- >Xint AddPass(uid_t uidUser, char *szPass) >X{ >X HIST_NODE *pEntry = GetHistEntry(uidUser); // User's hist node >X int i; // Loop control >X time_t timeNow = time(NULL); // Sec since epoch, now. >X >X // >X // I dunno why this would fail bug handle it if it does. >X // >X if (-1 == timeNow) >X { >X return(0); >X } >X >X // >X // This user has no history as far as we know. Make one.. do not >X // bother inserting it into the right place in the list as we will >X // not be querying the list again anyway... it will get sorted in >X // next time we read/sort the list (next time someone runs >X // passwd). >X // >X if (NULL == pEntry) >X { >X g_iHistCount++; >X g_pHist = realloc(g_pHist, g_iHistCount * sizeof(HIST_NODE)); >X pEntry = &(g_pHist[g_iHistCount - 1]); >X memset(pEntry, 0, sizeof(HIST_NODE)); >X pEntry->uid = uidUser; >X } >X >X // >X // Depending on whether we have a full (HIST_COUNT) list of >X // old passwords we will either add a new one or ripple the >X // oldest saved one off the back to make room for a new one. >X // >X if (pEntry->iNumPass < HIST_COUNT) >X { >X strncpy(pEntry->szPass[pEntry->iNumPass], szPass, PASS_MAX); >X pEntry->iNumPass++; >X } >X else >X { >X // >X // If we get here this guy's history is full. If the newest >X // password in the history is over HIST_TIME seconds old then >X // ripple-shift the passwords in his list and bump the oldest >X // one off the "no use" history list. >X // >X // However if the newest password is less than HIST_TIME >X // seconds old then just replace *it* with the new password. >X // That way if the guy is messing with us and has a script to >X // cycle through HIST_COUNT passwords in an effort to be able >X // to reuse the original one again it will not work. >X // Moreover, if the newest password has hardly been used >X // (since it is so young) it is safer to lose it than an older >X // one that has been used longer (presumably). >X // >X if (timeNow - pEntry->timeLastUpdate > HIST_TIME) >X { >X for (i = 1; i < HIST_COUNT; i++) >X { >X strncpy(pEntry->szPass[i - 1], pEntry->szPass[i], PASS_MAX); >X } >X } >X strncpy(pEntry->szPass[HIST_COUNT - 1], szPass, PASS_MAX); >X } >X >X // >X // Last update time is now! >X // >X pEntry->timeLastUpdate = timeNow; >X >X return(1); >X} >X >X >X//+---------------------------------------------------------------------------- >X// >X// Function: CheckPass >X// >X// Synopsis: Determine whether a given (cleartext) password that the user >X// wants to use matches any of the passwords in the history file. >X// >X// Arguments: uid_t uidUser - The uid of the user trying to change password >X// char *szPassword - The (cleartext) proposed new password. >X// >X// Returns: int - 1 if the password is good (not present in the user's >X// password history) or 0 if the password has already been >X// used by this user. >X// >X//+---------------------------------------------------------------------------- >Xint CheckPass(uid_t uidUser, char *szPassword) >X{ >X HIST_NODE *pEntry = GetHistEntry(uidUser); // History node pointer >X int i; // Loop control >X >X // >X // If we do not have a history for this particular user (they have >X // never had their password set before?) we will let them use this >X // password. >X // >X if (NULL == pEntry) >X { >X return(1); >X } >X else >X { >X // >X // Otherwise check out this guy's history for matches. >X // >X for (i = 0; i < pEntry->iNumPass; i++) >X { >X if (!strcmp(crypt(szPassword, pEntry->szPass[i]), >X pEntry->szPass[i])) >X { >X return(0); >X } >X } >X } >X // >X // No matches, this is a good password as far as the history is >X // concerned. >X // >X return(1); >X} >X >X >X//+---------------------------------------------------------------------------- >X// >X// Function: GetHistEntry >X// >X// Synopsis: A binary search on the history table looking for a record >X// that matches a particular uid target (param). >X// >X// Arguments: uid_t uidTarg - The uid we are seeking >X// >X// Returns: HIST_NODE* - A ptr to the record cooresponding to this uid, >X// NULL if uid was not found in the table. >X// >X//+---------------------------------------------------------------------------- >Xstatic HIST_NODE *GetHistEntry(uid_t uidTarg) >X{ >X unsigned int iLeft = 0; // The left (bottom) range idx >X unsigned int iRight = g_iHistCount; // The right (high) range idx >X unsigned int iMid = 0; // The midpoint >X int fFound = 0; // Did we find a match? >X uid_t uidPossible; // Value of a possible match. >X >X // >X // Pre: we should never have left > right. >X // >X if (iLeft > iRight) return(NULL); >X >X while ((iLeft <= iRight) && !fFound) >X { >X iMid = (iLeft + iRight) / 2; >X >X uidPossible = g_pHist[iMid].uid; >X if (uidPossible == uidTarg) >X { >X fFound = 1; >X } >X else >X { >X if (uidTarg < uidPossible) >X { >X iRight = iMid - 1; >X } >X else >X { >X iLeft = iMid + 1; >X } >X } >X } >X >X if (fFound) >X { >X return(&(g_pHist[iMid])); >X } >X else >X { >X return(NULL); >X } >X} >X >X >X >X >X >X >X >X >X >X >END-of-passhist.c >echo x - passwd.diff >sed 's/^X//' >passwd.diff << 'END-of-passwd.diff' >X*** passwd/Makefile Sat Dec 18 05:55:15 1999 >X--- passwd/Makefile Thu Jan 20 20:01:41 2000 >X*************** >X*** 26,32 **** >X >X PROG= passwd >X SRCS= local_passwd.c passwd.c pw_copy.c pw_util.c pw_yp.c \ >X! yp_passwd.c ypxfr_misc.c ${GENSRCS} >X GENSRCS=yp.h yp_clnt.c yppasswd.h yppasswd_clnt.c \ >X yppasswd_private.h yppasswd_private_clnt.c yppasswd_private_xdr.c >X CFLAGS+=-Wall >X--- 26,32 ---- >X >X PROG= passwd >X SRCS= local_passwd.c passwd.c pw_copy.c pw_util.c pw_yp.c \ >X! yp_passwd.c ypxfr_misc.c passhist.c ${GENSRCS} >X GENSRCS=yp.h yp_clnt.c yppasswd.h yppasswd_clnt.c \ >X yppasswd_private.h yppasswd_private_clnt.c yppasswd_private_xdr.c >X CFLAGS+=-Wall >X*************** >X*** 42,48 **** >X -I${.CURDIR}/../../usr.bin/chpass \ >X -I${.CURDIR}/../../libexec/ypxfr \ >X -I${.CURDIR}/../../usr.sbin/rpc.yppasswdd \ >X! -Dyp_error=warnx -DLOGGING >X >X .endif >X >X--- 42,48 ---- >X -I${.CURDIR}/../../usr.bin/chpass \ >X -I${.CURDIR}/../../libexec/ypxfr \ >X -I${.CURDIR}/../../usr.sbin/rpc.yppasswdd \ >X! -Dyp_error=warnx -DLOGGING -DPASS_HIST >X >X .endif >X >X*** passwd/local_passwd.c Fri Aug 27 18:04:51 1999 >X--- passwd/local_passwd.c Thu Jan 20 19:27:54 2000 >X*************** >X*** 68,73 **** >X--- 68,76 ---- >X #endif >X >X #include "extern.h" >X+ #ifdef PASS_HIST >X+ #include "passhist.h" >X+ #endif >X >X static uid_t uid; >X int randinit; >X*************** >X*** 101,106 **** >X--- 104,110 ---- >X #endif >X char buf[_PASSWORD_LEN+1], salt[10]; >X struct timeval tv; >X+ int fUsePasswdHist = 0; >X >X if (!nis) >X (void)printf("Changing local password for %s.\n", pw->pw_name); >X*************** >X*** 128,137 **** >X--- 132,161 ---- >X if (period > (time_t)0) { >X pw->pw_change = time(NULL) + period; >X } >X+ >X+ #ifdef PASS_HIST >X+ /* >X+ * Should this user be forced to use password history >X+ * mechanism? >X+ */ >X+ fUsePasswdHist = (int)login_getcapbool(lc, "usepasswordhist", 0); >X+ #endif >X+ >X login_close(lc); >X } >X #endif >X >X+ #ifdef PASS_HIST >X+ /* >X+ * If we are using password histories, initialize the list now. >X+ */ >X+ if (fUsePasswdHist) >X+ { >X+ InitializeList(); >X+ ReadList(); >X+ } >X+ #endif >X+ >X for (buf[0] = '\0', tries = 0;;) { >X p = getpass("New password:"); >X if (!*p) { >X*************** >X*** 147,152 **** >X--- 171,189 ---- >X (void)printf("Please don't use an all-lower case password.\nUnusual capitalization, control characters or digits are suggested.\n"); >X continue; >X } >X+ >X+ #ifdef PASS_HIST >X+ if (fUsePasswdHist) >X+ { >X+ if ((0 == CheckPass(pw->pw_uid, p)) && (uid != 0 || ++tries < 2)) >X+ { >X+ printf("Please do not reuse passwords you have already used " >X+ "recently.\n"); >X+ continue; >X+ } >X+ } >X+ #endif >X+ >X (void)strcpy(buf, p); >X if (!strcmp(buf, getpass("Retype new password:"))) >X break; >X*************** >X*** 180,185 **** >X--- 217,237 ---- >X salt[8] = '\0'; >X } >X #endif >X+ >X+ #ifdef PASS_HIST >X+ /* >X+ * If we are using password histories, add this new password to the >X+ * user's history list (so he can't re-use it later for a while) and >X+ * write/close the list. >X+ */ >X+ if (fUsePasswdHist) >X+ { >X+ AddPass(pw->pw_uid, crypt(buf, salt)); >X+ WriteList(); >X+ CloseList(); >X+ } >X+ #endif >X+ >X return (crypt(buf, salt)); >X } >X >X*** passwd/passwd.1 Fri Aug 27 18:04:51 1999 >X--- passwd/passwd.1 Thu Jan 20 20:04:42 2000 >X*************** >X*** 90,96 **** >X is set according to >X .if t ``passwordtime'' >X .if n "passwordtime" >X! capability in the user's login class. >X .Pp >X To change another user's Kerberos password, one must first >X run >X--- 90,101 ---- >X is set according to >X .if t ``passwordtime'' >X .if n "passwordtime" >X! capability in the user's login class. The boolean >X! .if t ``usepasswordhist'' >X! .if n "usepasswordhist" >X! capability's presence in a user's login class indicates that >X! the user's new password will be checked by the password history >X! mechanism, which will not allow the reuse of recent old passwords. >X .Pp >X To change another user's Kerberos password, one must first >X run >X*************** >X*** 199,204 **** >X--- 204,211 ---- >X The user database >X .It Pa /etc/passwd >X A Version 7 format password file >X+ .It Pa /etc/passwd.hist >X+ Password history file >X .It Pa /etc/passwd.XXXXXX >X Temporary copy of the password file >X .It Pa /etc/login.conf >END-of-passwd.diff >exit
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Raw
Actions:
View
Attachments on
bug 16244
: 7499