I'm writing a script which pulls out some patient data and generates an XML export.
Each patient record has an associated doctor - but rather than repeat doctor details in each record, I figured I'd set the doctor ID in the patient record, and then include a list of doctors in a different section at the bottom of the document.
One thing I need to do is included a GUID for the doctor in the patient record, but the actual database relationship is a local non-unique ID. I figured the best way forward was to map the GUIDs in a list of local IDs using a dictionary.
Anyway, long story short, here is the bit that builds the require list:
While Not PatientRec.EOF
Set DoctorRec = MyDB.OpenRecordset("Select Lng_Key, Txt_GUID From Tbl_LU_DoctorDetail Where Lng_Key = " & PatientRec![Lng_Doctor])
While Not DoctorRec.EOF
If (IsNull(DoctorRec![Txt_GUID])) Then
DoctorRec.Edit
DoctorRec![Txt_GUID] = CreateGUID()
DoctorRec.Update
End If
DoctorList.Add DoctorRec![Lng_Key], DoctorRec![Txt_GUID]
' outputs something like '5:{03f50fe1-a0a4-4733-906a-771e22845ea6}
MsgBox (DoctorRec![Lng_Key] & ":" & DoctorList.Items(DoctorRec![Lng_Key]))
DoctorRec.MoveNext
Wend
Wend
' outputs nothing!
MsgBox (DoctorList.Item(5))
' but there is something in there???
MsgBox (DoctorList.count)
I've also tried casting the id to a string using CStr, but get the same result with DoctorList.Item("5")
Worse, when I try:
Dim v As Variant
For Each v In DoctorList.Keys
MsgBox (v & ":" & DoctorList.Item(v))
Next
I get the error:
Run-time error '3420':
Object invalid or no longer set.
Testing (and the helpfile) indicates that the Variant 'v' is not being set to anything from the Keys property, but the For Each is at least attempting on loop...
-- Update
I found a similar question by someone on vbforums: http://www.vbforums.com/showthread.php?t=622933
I tested with a hardcoded key and item:
DoctorList.Add 5, "String"
The For Each loop now runs once successfully, but then fails with the 3420 error on a second loop (even when it should have stopped on the first loop).
Found the problem - it appears that Dictionaries will happily use objects as keys, so when using the Dictionary.Add method you have to explicitly use the Value property of the field from the recordset:
DoctorList.Add DoctorRec![Lng_Key].Value, DoctorRec![Txt_GUID].Value
In regard to GUIDs, Access doesn't like them. Michael Kaplan wrote about this years and years ago. You might want to look into the StringFromGUID() and GUIDToString() functions.
And if there is not an external requirement that you use GUIDs, you should seriously consider getting rid of them entirely. They don't add anything at all that is necessary in 99.99% of Access applications.
Secondly, I've never used the scripting runtime's dictionary, but it really looks to me like it offers nothing you can't already get with a VBA custom collection. Can you outline what you're using it for, and how it is superior to the VBA collection? Also, why are you using early binding and not late binding?
Related
I am the maintainer (but thankfully not the creator) of a very old, very large and very badly written classic ASP site for an electronics manufacturer.
Security is a joke. This is the only thing done to sanitize input before throwing it into the mouth of MySQL:
Function txtval(data)
txtval = replace(data,"'","'")
txtval = trim(txtval)
End Function
productid = txtval(Request.QueryString("id"))
SQL = "SELECT * FROM products WHERE id = " & productid
Set rs = conn.execute(SQL)
Because of that, the site is unfortunately (but perhaps not surprisingly) victim of SQL injection attacks, some of them succesful.
The simple means taken above is not nearly enough. Nor is using Server.HTMLEncode. Escaping slashes doesn't help either as the attacks are quite sophisticated:
product.asp?id=999999.9+UnIoN+AlL+SeLeCt+0x393133353134353632312e39,0x393133353134353632322e39,0x393133353134353632332e39,0x393133353134353632342e39,0x393133353134353632352e39,0x393133353134353632362e39
The url above (an arbitrary attempt taken from the access log) gives the folling response from the site:
Microsoft OLE DB Provider for ODBC Drivers error '80004005'
[MySQL][ODBC 5.3(w) Driver][mysqld-5.1.42-community]
The used SELECT statements have a different number of columns
/product.asp, line 14
This means that the injection made it through but in this case did not succeed in getting any data. Others do, however.
The site consists of hundreds of ASP files with spaghetti code summing up to many thousands of lines without much structure. Because of that it is not an option to go for parameterized queries. The work would be enormous and error prone as well.
One good thing though is that all input parameters in the code are consistently passed through the txtval function, so here is a chance to do it better by augmenting the function. Also, since all SQL calls are done with conn.execute(SQL) it is quite straightforward to search and replace with eg. conn.execute(sanitize(SQL)) so here is a chance to do something about it too.
Given the circumstances, what are my options to prevent or at least minimize the risc of SQL injection?
Any input is much appreciated.
Updates:
1.
I do understand that parameterized queries is the correct way to handle the problem. I use that myself when I create websites. But given the way the site is built and the size of it, it will take 1-2 months to modify, test and debug it. Even if that is what we end up with (which I doubt) I need to do something right now.
2.
The replacement with the html entity is not a typo. It replaces single quote with its html entity. (I didn't make the code!)
3.
In the specific example above, using CInt(id) would solve the problem, but it could be anything, not only numerical inputs.
UPDATE 2:
Ok, I know that I am not asking for the correct solution. I knew that from the start. That's why I wrote "Given the circumstances".
But still, filtering inputs for mysql keywords like select, union etc would at least make it better. Not good, but a little bit better. And this is what I am asking for, ideas to make it a little bit better.
Although I appreciate your comments, telling me that the only good option is to use parameterized queries doesn't really help. Because I know that already :)
I wouldn't give up on parameterized queries. They are the single best tool you can use to protect yourself from SQL Injection. If your plan is to replace all of these calls:
conn.execute(SQL)
to these calls:
conn.execute(sanitize(SQL))
then you're already looking at modifying each interaction with SQL (BTW, don't forget Command.Execute() and Recordset.Open(), which may also be used to run SQL statements). And since you're already planning on changing these calls, consider calling a custom function to run the statement. For example, replace:
set rs = conn.execute(SQL)
with:
set rs = MyExecute(SQL)
and then use your custom function to set up a proper parameterized query using a Command object instead. You'll need to cleverly parse the SQL statement in this custom function. Identify the values in the where clause, determine their type (perhaps you can query the table schema), and add parameters accordingly. But it can be done.
You can also take this opportunity to sanitize the input. Use a RegExp object to quickly strip [^0-9\.] from numeric fields, for example.
But there's still the opportunity that you'll return a recordset from this function that will be used to write values directly to the page without being HTML-encoded first. That's a real concern, especially since it sounds like your site has already been targeted in the past. I wouldn't trust any data coming from your database. The only option I see here (that wouldn't involve touching every page) is to return a "clean", HTML-encoded recordset instead of the default one.
Unfortunately, you're still not out of the woods. XSS attacks can be done via QueryString parameters, cookies, and form controls. How safe are you going to feel after "fixing" the SQL Injection issues knowing that XSS is still a very real possibility?
My advice? Explain to your supervisor the security threats plaguing your site and convince him/her the need for a thorough review or a complete rewrite. It may seem like a lot of resources to throw at an "old, already-working website", but the moment someone defaces your website or truncates your database tables, you'll wish you invested the time.
This attack should only affect numeric values passed in your SQL.
There may or may not be a quick fix depending on whether the same txtval function is used for both numeric and string values (and others like date too).
If txtval is only used for numeric values (probably unlikely) then you could protected by adding single quotes around the value, eg:
Function txtval(data)
txtval = replace(data,"'","'")
txtval = "'" & trim(txtval) & "'"
End Function
If it is used for all value types then your only option might be to search through all the code and either:
1) Add single quotes to all numeric SQL, eg:
SQL = "SELECT * FROM products WHERE id = '" & productid & "'"
2) Create a new function just for sanitizing number values and then change all your queries to use that (not a quick fix), eg:
Function numval(data)
If IsNumeric(data) Then
numvalue = CDbl(data)
Else
numvalue = 0 'or NULL?
End If
End Function
And then change your queries, eg:
productid = numval(Request.QueryString("id"))
SQL = "SELECT * FROM products WHERE id = " & productid
Is there common code (ie. in an include file) that is used to open the database and create the conn variable used in your sample code?
If so, then you could just replace that code and create your own class with Open, Close and Execute functions (at least). You may need other methods too if they are used in your code.
That way you could effectively override the execute in lines like Set rs = conn.execute(SQL).
Eg:
Class MyDatabase
Private m_conn
Public Sub Open(connString)
Set m_conn = Server.CreateObject("ADODB.Connection")
m_conn.Open connString
End Sub
Public Sub Close()
m_conn.Close
Set m_conn = Nothing
End Sub
Public Function Execute(sql)
'Sanitize input here (sql), simple example just for this type of attack
If InStr(sql, "UnIoN AlL SeLeCt") <> 0 Then sql = ""
'return a RecordSet
Set Execute = m_conn.Execute(sql)
End Property
End Class
Then change your common conn declaration from... (eg)
Set conn = Server.CreateObject("ADODB.Connection")
...to...
Set conn = New MyDatabase
If you keep the txtval function I would also update it to escape slashes as well as single quotes, eg:
Function txtval(data)
txtval = Replace(Replace(strValue, "'", "''"), "\", "\\")
txtval = trim(txtval)
End Function
Hopefully something here might be of help.
I'm trying to use a macro to check and see if there are records left within a table, and if there are, then display a message, letting me know there is still data in the table. My set up looks like this
Currently, I'm getting this error message:
I get this error message regardless of whether I use Is Not Null or Not IsNull. Is there any suggestions as to what I'm doing wrong?
Macros is one of the, if not the, most annoying topics in Access. It illustrates beautifully one of the most galactically stupid aspects of Access: it cheerfully allows you to point to and select a table by name and a field name to boot giving you the false hope that it actually knows what you are talking about then turns around and tells you it has no idea what you are asking for. Brilliant. Not only does the "red headed step child" that is Access get mismatched shoes, neither of them fit!
I honestly thought this would be easy to answer. But to add insult to injury, Microsoft has documented this poorly conceived "feature" about as well as they have implemented it.
I'm not sure I understand exactly what you are trying to do but I think you simply want to know if a table is empty. If so, my suggestion: use a function. For example:
Function IsTableEmpty(aTableName As String) As Boolean
Dim rst As DAO.Recordset
Set rst = CurrentDb.OpenRecordset("select count(*) from [" & aTableName & "];", dbOpenForwardOnly, dbReadOnly)
IsTableEmpty = rst.Fields(0) = 0
rst.Close
Set rst = Nothing
End Function
If you insist on using a macro you will be astonished to find that Access can find your function and use it with no difficulty. You can use this with any table (query for that matter).
Sadly, if you are doing web applications with Access you are going to have to come to grips with whatever (lame) functionality macros provide because VBA will not be available to you.
HTH. Good luck.
PS - I would happily eat my words if someone would point me to the documentation that explains how this works and why such a situation exists!
I needed to copy some values from MS Access table into Excel using VBA code. I had done this many times and considered myself experienced. In my code I export the data using the following statements:
sh.range("A" & row).Value = rs("MyField")
where sh is Excel sheet, row is integer (long) and rs is recordset (either DAO or ADO, without any effect on the problem under consideration).
My code worked well on my computer with installed MS Office 2007. But when my client ran the code on his machine with MS Office 2010, it failed and kept failing very randomly. E.g. when debugging VBA in MS Access module step by step by pressing F8 it always worked. But when I pressed 'run' F5, it failed very soon.
After many trials and errors (I tried to open the recordset using different options and recordset types and caching the records etc.), I finally found that if I write
sh.range("A" & row).Value = rs("MyField").Value
everything works just fine. But according to the docs (e.g. http://msdn.microsoft.com/en-us/library/office/ff197799(v=office.15).aspx ) the Value property is the default property of the field object, which in turn is the default collection of the recordset object.
But it seems that I cannot rely on the defaultness, which I have been doing in most of my code. Actually I found a solution to my problem, but I still have no idea about the cause. Does anyone know why is the code doing this? What is the problem?
PS: I also found that if I expand the one-line statement into two lines (three with declaration):
dim v as Variant
v = rs("MyField")
sh.range("A" & row).Value = v
it also works...
Since rs("MyField") is a Field object, if you do ...
MsgBox TypeName(rs("MyField"))
... Access will tell you its type is Field.
So TypeName() is one example where the object itself is referenced directly instead of its default .Value property.
But something like Debug.Print always references .Value, so Debug.Print rs("MyField") is the same as Debug.Print rs("MyField").Value
If you know exactly when .Value will be referenced implicitly and when it will not, you can add it only when absolutely required and omit it the rest of the time.
However, some Access developers recommend always including .Value to avoid such confusion. If that seems like too much effort to you, at least consider including .Value when you do any assignment ...
something = rs("MyField").Value
... and be watchful for any other contexts where you don't get what you want without .Value
Okay, friends, I'm leaving my job in a week and a half, and I'm trying to make what I've done easier for my boss to do. He has no access knowledge, so I'm trying to create a form that will automate the reports I've been generating. Rather than create a different form for all the different reports, I'm trying to automate it from a table of parameters. Here's what I'm going for:
I have a table, which I have created, which is comprised of 5 fields. I'd like to use these fields to fill parameter fields in a standard form template. The five fields in my table are as follows:
The type of query being run (the result spit out)
The queries that generate this report, separated by a comma and no space. "QRYNAMEA,QRYNAMEB"
The Table which these queries generate, which will be used by transferspreadsheet
The destination excel file, which already has a pivot table set up to feed of the data.
The input sheet of this excel file. Currently, all of these sheets are called "Input". (that isn't important)
My issue comes with having no idea where to go after I've made my combo box. I know enough visual basic to automate my queries, but not enough to populate the form with the information in 3,4 and 5 (so far, I've been manually changing these for different queries). I have no idea how to look up the record in the table from the choice in the 'choosebox', and then select individual fields from that in my automation.
I'm pretty confident in my ability to parse #2 and automate the queries, and to put the values into the fields I'm looking at, but I don't know how to actually pull those values from the table, before I can do these things. I also can't seem to describe this well enough for google to help me.
Has anyone done something like this before? I'm assuming I just lack knowledge of one of the VBA libraries, but I've not had any luck finding out which.
edit:
my inclination at this point is to create a query for this table, which will return a single field depending on the input I give. I can imagine doing this in SQL, but I still don't know how to populate the forms, nor extract the field object from the table once I get it.
I have to head out for the day, but I'll be back on Friday to keep working on this, and I'll post my solution, once I find it. This seems like a unique conundrum, and it would be nice to give an answer to it.
Final edit: code is polished (does not have much in the way of error handling):
The first method, which pulls the fields from the table and populates the form, is activated by choosing a new entry in the combo box and looks like this:
Private Sub QuerySelect_Change()
Dim db As Database
Dim rec As Recordset
Set db = CurrentDb
Set rec = db.OpenRecordset("SELECT [Queries to Run], [Source Table], [Destination Spreadsheet], [Destination Sheet Name] FROM TBL_QRY_SETTINGS WHERE TBL_QRY_SETTINGS.[Query Type] Like '" & [Forms]![QuerySelector]![QuerySelect] & "';")
[Forms]![QuerySelector]![QueriesToRun].Value = rec("Queries to Run")
[Forms]![QuerySelector]![SourceTable].Value = rec("Source Table")
[Forms]![QuerySelector]![FileDest].Value = rec("Destination Spreadsheet")
[Forms]![QuerySelector]![SheetName].Value = rec("Destination Sheet Name")
Set rec = Nothing
Set db = Nothing
End Sub
The second code pulls that data to run the query. I like how this turned out. It runs when a button near the combobox is clicked.
Private Sub DynamicQuery_Click()
Dim qryArray As Variant
Dim i As Integer
qryArray = Split([Forms]![QuerySelector]![QueriesToRun], ",")
DoCmd.SetWarnings False
For i = LBound(qryArray) To UBound(qryArray)
Debug.Print qryArray(i)
DoCmd.OpenQuery (qryArray(i))
Next
DoCmd.SetWarnings True
DoCmd.TransferSpreadsheet acExport, acSpreadsheetTypeExcel12Xml, [Forms]![QuerySelector]![SourceTable], _
[Forms]![QuerySelector]![FileDest], _
True, [Forms]![QuerySelector]![SheetName]
End Sub
Note that the final code for part (1) is almost the same as the selected answer, except that I am grabbing more than one field. This works because I know that I have unique "Query Types", and my recordset will only contain one record.
Anyway, I hope some people stumble upon this and find it useful. Send me a message if you do. As far as I can tell from brief googling, this sort of automation work has not been done in access. It should make it easier for access-illiterate to run their own queries, and be simple for designers to add to, if they want all their queries available after a few clicks.
Someone could conceivably use this to automate a variety of reports in sequence, by iterating through a table like the one I reference.
I may be massively misunderstanding what you're doing, but I think it's as easy as creating a new form using the form wizard. It will let you choose the table that contains the data, and it will let you choose which fields you want to add.
You can later change any of the textboxes to combo boxes which will allow you to limit the choices available to fill in.
Am I understanding that correctly?
EDIT: This will fill a variable (MyRandomField) with the contents of a field in a table
Dim db as Database
Dim rec as Recordset
set db = CurrentDB
set rec = db.OpenRecordSet("Select SomeField from SomeTable Where Something = 'SomethingElse'")
MyRandomField = rec("SomeFieldName")
set rec = Nothing
set db = Nothing
How do I find broken queries in access.
i.e. Queries that might have broken because the underlying table was deleted or the name of the column in the table changed?
Is there an easy way -- rather than just opening each query running and checking if something has gone wrong?
Here are a few notes that may be of interest, depending on your version of Access.
See: GetDependencyInfo Method [Access 2003 VBA Language Reference]
Do not forget that Track name AutoCorrect info is not a good thing, for the most part, but can be useful in certain circumstances.
Dim dinf As DependencyInfo
For j = 0 To CurrentData.AllQueries.Count - 1
Set dinf = CurrentData.AllQueries(j).GetDependencyInfo
For i = 0 To dinf.Dependencies.Count - 1
''Missing alias, query or table, as far as I can tell
If dinf.Dependencies.Item(i).Name Like "MISSING:*" Then
Debug.Print CurrentData.AllQueries(j).Name _
& " " & dinf.Dependencies.Item(i).Name
End If
Next
Next
You may need to update dependencies:
Application.CurrentProject.UpdateDependencyInfo
This will require a save.